]> git.mxchange.org Git - flightgear.git/blob - src/GUI/BaseDiagram.cxx
Labels on large airports in the diagram
[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 #include "QtLauncher_fwd.hxx"
35
36 /* equatorial and polar earth radius */
37 const float rec  = 6378137;          // earth radius, equator (?)
38 const float rpol = 6356752.314f;      // earth radius, polar   (?)
39
40 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
41 static float earth_radius_lat( float lat )
42 {
43     double a = cos(lat)/rec;
44     double b = sin(lat)/rpol;
45     return 1.0f / sqrt( a * a + b * b );
46 }
47
48 BaseDiagram::BaseDiagram(QWidget* pr) :
49     QWidget(pr),
50     m_autoScalePan(true),
51     m_wheelAngleDeltaAccumulator(0)
52 {
53     setSizePolicy(QSizePolicy::MinimumExpanding,
54                   QSizePolicy::MinimumExpanding);
55     setMinimumSize(100, 100);
56 }
57
58 QTransform BaseDiagram::transform() const
59 {
60     QTransform t;
61     t.translate(width() / 2, height() / 2); // center projection origin in the widget
62     t.scale(m_scale, m_scale);
63
64     // apply any pan offset that exists
65     t.translate(m_panOffset.x(), m_panOffset.y());
66     // center the bounding box (may not be at the origin)
67     t.translate(-m_bounds.center().x(), -m_bounds.center().y());
68     return t;
69 }
70
71 void BaseDiagram::clearIgnoredNavaids()
72 {
73     m_ignored.clear();
74 }
75
76 void BaseDiagram::addIgnoredNavaid(FGPositionedRef pos)
77 {
78     if (isNavaidIgnored(pos))
79         return;
80     m_ignored.push_back(pos);
81 }
82
83 void BaseDiagram::extendRect(QRectF &r, const QPointF &p)
84 {
85     if (p.x() < r.left()) {
86         r.setLeft(p.x());
87     } else if (p.x() > r.right()) {
88         r.setRight(p.x());
89     }
90
91     if (p.y() < r.top()) {
92         r.setTop(p.y());
93     } else if (p.y() > r.bottom()) {
94         r.setBottom(p.y());
95     }
96 }
97
98 void BaseDiagram::paintEvent(QPaintEvent* pe)
99 {
100     QPainter p(this);
101     p.setRenderHints(QPainter::Antialiasing);
102     p.fillRect(rect(), QColor(0x3f, 0x3f, 0x3f));
103
104     if (m_autoScalePan) {
105         // fit bounds within our available space, allowing for a margin
106         const int MARGIN = 32; // pixels
107         double ratioInX = (width() - MARGIN * 2) / m_bounds.width();
108         double ratioInY = (height() - MARGIN * 2) / m_bounds.height();
109         m_scale = std::min(ratioInX, ratioInY);
110     }
111
112     QTransform t(transform());
113     p.setTransform(t);
114
115     paintNavaids(&p);
116
117     paintContents(&p);
118 }
119
120 void BaseDiagram::paintAirplaneIcon(QPainter* painter, const SGGeod& geod, int headingDeg)
121 {
122     QPointF pos = project(geod);
123     QPixmap pix(":/airplane-icon");
124     pos = painter->transform().map(pos);
125     painter->resetTransform();
126     painter->translate(pos.x(), pos.y());
127     painter->rotate(headingDeg);
128
129     painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
130     QRect airplaneIconRect = pix.rect();
131     airplaneIconRect.moveCenter(QPoint(0,0));
132     painter->drawPixmap(airplaneIconRect, pix);
133 }
134
135 class MapFilter : public FGPositioned::TypeFilter
136 {
137 public:
138
139     MapFilter(LauncherAircraftType aircraft)
140     {
141       //  addType(FGPositioned::FIX);
142         addType(FGPositioned::NDB);
143         addType(FGPositioned::VOR);
144
145         if (aircraft == Helicopter) {
146             addType(FGPositioned::HELIPAD);
147         }
148
149         if (aircraft == Seaplane) {
150             addType(FGPositioned::SEAPORT);
151         } else {
152             addType(FGPositioned::AIRPORT);
153         }
154     }
155
156     virtual bool pass(FGPositioned* aPos) const
157     {
158         bool ok = TypeFilter::pass(aPos);
159         // fix-filtering code disabled since fixed are entirely disabled
160 #if 0
161         if (ok && (aPos->type() == FGPositioned::FIX)) {
162             // ignore fixes which end in digits
163             if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
164                 return false;
165             }
166         }
167 #endif
168         return ok;
169     }
170 };
171
172 void BaseDiagram::splitItems(const FGPositionedList& in, FGPositionedList& navaids,
173                              FGPositionedList& ports)
174 {
175     FGPositionedList::const_iterator it = in.begin();
176     for (; it != in.end(); ++it) {
177         if (FGAirport::isAirportType(it->ptr())) {
178             ports.push_back(*it);
179         } else {
180             navaids.push_back(*it);
181         }
182     }
183 }
184
185 bool orderAirportsByRunwayLength(const FGPositionedRef& a,
186                                  const FGPositionedRef& b)
187 {
188     FGAirport* aptA = static_cast<FGAirport*>(a.ptr());
189     FGAirport* aptB = static_cast<FGAirport*>(b.ptr());
190
191     return aptA->longestRunway()->lengthFt() > aptB->longestRunway()->lengthFt();
192 }
193
194 void BaseDiagram::paintNavaids(QPainter* painter)
195 {
196     QTransform xf = painter->transform();
197     painter->setTransform(QTransform()); // reset to identity
198     QTransform invT = xf.inverted();
199
200
201     SGGeod topLeft = unproject(invT.map(rect().topLeft()), m_projectionCenter);
202     SGGeod viewCenter = unproject(invT.map(rect().center()), m_projectionCenter);
203     SGGeod bottomRight = unproject(invT.map(rect().bottomRight()), m_projectionCenter);
204
205     double drawRangeNm = std::max(SGGeodesy::distanceNm(viewCenter, topLeft),
206                                   SGGeodesy::distanceNm(viewCenter, bottomRight));
207
208     MapFilter f(m_aircraftType);
209     FGPositionedList items = FGPositioned::findWithinRange(viewCenter, drawRangeNm, &f);
210
211     FGPositionedList navaids, ports;
212     splitItems(items, navaids, ports);
213
214     if (ports.size() >= 40) {
215         FGPositionedList::iterator middle = ports.begin() + 40;
216         std::partial_sort(ports.begin(), middle, ports.end(),
217                           orderAirportsByRunwayLength);
218         ports.resize(40);
219     }
220
221     m_labelRects.clear();
222     m_labelRects.reserve(items.size());
223
224     FGPositionedList::const_iterator it;
225     for (it = ports.begin(); it != ports.end(); ++it) {
226         paintNavaid(painter, xf, *it);
227     }
228
229     for (it = navaids.begin(); it != navaids.end(); ++it) {
230         paintNavaid(painter, xf, *it);
231     }
232
233
234     // restore transform
235     painter->setTransform(xf);
236 }
237
238 QRect boundsOfLines(const QVector<QLineF>& lines)
239 {
240     QRect r;
241     Q_FOREACH(const QLineF& l, lines) {
242         r = r.united(QRectF(l.p1(), l.p2()).toRect());
243     }
244
245     return r;
246 }
247
248 void BaseDiagram::paintNavaid(QPainter* painter, const QTransform& t, const FGPositionedRef &pos)
249 {
250     if (isNavaidIgnored(pos))
251         return;
252
253     bool drawAsIcon = true;
254     const double minRunwayLengthFt = (16 / m_scale) * SG_METER_TO_FEET;
255     const FGPositioned::Type ty(pos->type());
256     const bool isNDB = (ty == FGPositioned::NDB);
257     QRect iconRect;
258
259     if (ty == FGPositioned::AIRPORT) {
260         FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
261         if (apt->hasHardRunwayOfLengthFt(minRunwayLengthFt)) {
262
263             drawAsIcon = false;
264             painter->setTransform(t);
265             QVector<QLineF> lines = projectAirportRuwaysWithCenter(apt, m_projectionCenter);
266
267             QPen pen(QColor(0x03, 0x83, 0xbf), 8);
268             pen.setCosmetic(true);
269             painter->setPen(pen);
270             painter->drawLines(lines);
271
272             QPen linePen(Qt::white, 2);
273             linePen.setCosmetic(true);
274             painter->setPen(linePen);
275             painter->drawLines(lines);
276
277             painter->resetTransform();
278
279             iconRect = t.mapRect(boundsOfLines(lines));
280         }
281     }
282
283     if (drawAsIcon) {
284         QPixmap pm = iconForPositioned(pos);
285         QPointF loc = t.map(project(pos->geod()));
286         iconRect = pm.rect();
287         iconRect.moveCenter(loc.toPoint());
288         painter->drawPixmap(iconRect, pm);
289     }
290
291    // compute label text so we can measure it
292     QString label;
293     if (FGAirport::isAirportType(pos.ptr())) {
294         label = QString::fromStdString(pos->name());
295         label = fixNavaidName(label);
296     } else {
297         label = QString::fromStdString(pos->ident());
298     }
299
300     if (ty == FGPositioned::NDB) {
301         FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
302         label.append("\n").append(QString::number(nav->get_freq() / 100));
303     } else if (ty == FGPositioned::VOR) {
304         FGNavRecord* nav = static_cast<FGNavRecord*>(pos.ptr());
305         label.append("\n").append(QString::number(nav->get_freq() / 100.0, 'f', 1));
306     }
307
308     QRect textBounds = painter->boundingRect(QRect(0, 0, 100, 100),
309                                              Qt::TextWordWrap, label);
310     int textFlags;
311     textBounds = rectAndFlagsForLabel(pos->guid(), iconRect,
312                                       textBounds.size(),
313                                       textFlags);
314
315     painter->setPen(isNDB ? QColor(0x9b, 0x5d, 0xa2) : QColor(0x03, 0x83, 0xbf));
316     painter->drawText(textBounds, textFlags, label);
317 }
318
319 bool BaseDiagram::isNavaidIgnored(const FGPositionedRef &pos) const
320 {
321     return m_ignored.contains(pos);
322 }
323
324 bool BaseDiagram::isLabelRectAvailable(const QRect &r) const
325 {
326     Q_FOREACH(const QRect& lr, m_labelRects) {
327         if (lr.intersects(r))
328             return false;
329     }
330
331     return true;
332 }
333
334 int BaseDiagram::textFlagsForLabelPosition(LabelPosition pos)
335 {
336 #if 0
337     switch (pos) {
338     case LABEL_RIGHT:       return Qt::AlignLeft | Qt::AlignVCenter;
339     case LABEL_ABOVE:       return Qt::AlignHCenter | Qt::A
340     }
341 #endif
342     return 0;
343 }
344
345 QRect BaseDiagram::rectAndFlagsForLabel(PositionedID guid, const QRect& item,
346                                         const QSize &bounds,
347                                         int& flags) const
348 {
349     m_labelRects.append(item);
350     int pos = m_labelPositions.value(guid, LABEL_RIGHT);
351     bool firstAttempt = true;
352     flags = Qt::TextWordWrap;
353
354     while (pos < LAST_POSITION) {
355         QRect r = labelPositioned(item, bounds, static_cast<LabelPosition>(pos));
356         if (isLabelRectAvailable(r)) {
357             m_labelRects.append(r);
358             m_labelPositions[guid] = static_cast<LabelPosition>(pos);
359             flags |= textFlagsForLabelPosition(static_cast<LabelPosition>(pos));
360             return r;
361         } else if (firstAttempt && (pos != LABEL_RIGHT)) {
362             pos = LABEL_RIGHT;
363         } else {
364             ++pos;
365         }
366
367         firstAttempt = false;
368     }
369
370     return QRect(item.x(), item.y(), bounds.width(), bounds.height());
371 }
372
373 QRect BaseDiagram::labelPositioned(const QRect& itemRect,
374                                    const QSize& bounds,
375                                    LabelPosition lp) const
376 {
377     const int SHORT_MARGIN = 4;
378     const int DIAGONAL_MARGIN = 12;
379
380     QPoint topLeft = itemRect.topLeft();
381
382     switch (lp) {
383     // cardinal compass points are short (close in)
384     case LABEL_RIGHT:
385         topLeft = QPoint(itemRect.right() + SHORT_MARGIN,
386                      itemRect.center().y() - bounds.height() / 2);
387         break;
388     case LABEL_ABOVE:
389         topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
390                      itemRect.top() - (SHORT_MARGIN + bounds.height()));
391         break;
392     case LABEL_BELOW:
393         topLeft = QPoint(itemRect.center().x() - (bounds.width() / 2),
394                      itemRect.bottom() + SHORT_MARGIN);
395         break;
396     case LABEL_LEFT:
397         topLeft = QPoint(itemRect.left() - (SHORT_MARGIN + bounds.width()),
398                      itemRect.center().y() - bounds.height() / 2);
399         break;
400
401     // first diagonals are further out (to hopefully have a better chance
402     // of finding clear space
403
404     case LABEL_NE:
405         topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
406                      itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
407         break;
408
409     case LABEL_NW:
410         topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
411                      itemRect.top() - (DIAGONAL_MARGIN + bounds.height()));
412         break;
413
414     case LABEL_SE:
415         topLeft = QPoint(itemRect.right() + DIAGONAL_MARGIN,
416                      itemRect.bottom() + DIAGONAL_MARGIN);
417         break;
418
419     case LABEL_SW:
420         topLeft = QPoint(itemRect.left() - (DIAGONAL_MARGIN + bounds.width()),
421                      itemRect.bottom() + DIAGONAL_MARGIN);
422         break;
423     default:
424         qWarning() << Q_FUNC_INFO << "Implement me";
425
426     }
427
428     return QRect(topLeft, bounds);
429 }
430
431 void BaseDiagram::mousePressEvent(QMouseEvent *me)
432 {
433     m_lastMousePos = me->pos();
434     m_didPan = false;
435 }
436
437 void BaseDiagram::mouseMoveEvent(QMouseEvent *me)
438 {
439     m_autoScalePan = false;
440
441     QPointF delta = me->pos() - m_lastMousePos;
442     m_lastMousePos = me->pos();
443
444     // offset is stored in metres so we don't have to modify it when
445     // zooming
446     m_panOffset += (delta / m_scale);
447     m_didPan = true;
448
449     update();
450 }
451
452 int intSign(int v)
453 {
454     return (v == 0) ? 0 : (v < 0) ? -1 : 1;
455 }
456
457 void BaseDiagram::wheelEvent(QWheelEvent *we)
458 {
459     m_autoScalePan = false;
460
461     int delta = we->angleDelta().y();
462     if (delta == 0)
463         return;
464
465     if (intSign(m_wheelAngleDeltaAccumulator) != intSign(delta)) {
466         m_wheelAngleDeltaAccumulator = 0;
467     }
468
469     m_wheelAngleDeltaAccumulator += delta;
470     if (m_wheelAngleDeltaAccumulator > 120) {
471         m_wheelAngleDeltaAccumulator = 0;
472         m_scale *= 2.0;
473     } else if (m_wheelAngleDeltaAccumulator < -120) {
474         m_wheelAngleDeltaAccumulator = 0;
475         m_scale *= 0.5;
476     }
477
478     update();
479 }
480
481 void BaseDiagram::paintContents(QPainter* painter)
482 {
483 }
484
485 void BaseDiagram::recomputeBounds(bool resetZoom)
486 {
487     m_bounds = QRectF();
488     doComputeBounds();
489
490     if (resetZoom) {
491         m_autoScalePan = true;
492         m_scale = 1.0;
493         m_panOffset = QPointF();
494     }
495
496     update();
497 }
498
499 void BaseDiagram::doComputeBounds()
500 {
501     // no-op in the base class
502 }
503
504 void BaseDiagram::extendBounds(const QPointF& p)
505 {
506     extendRect(m_bounds, p);
507 }
508
509 QPointF BaseDiagram::project(const SGGeod& geod, const SGGeod& center)
510 {
511     double r = earth_radius_lat(geod.getLatitudeRad());
512     double ref_lat = center.getLatitudeRad(),
513     ref_lon = center.getLongitudeRad(),
514     lat = geod.getLatitudeRad(),
515     lon = geod.getLongitudeRad(),
516     lonDiff = lon - ref_lon;
517
518     double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
519     if (c == 0.0) {
520         // angular distance from center is 0
521         return QPointF(0.0, 0.0);
522     }
523
524     double k = c / sin(c);
525     double x, y;
526     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
527     {
528         x = (SGD_PI / 2 - lat) * sin(lonDiff);
529         y = -(SGD_PI / 2 - lat) * cos(lonDiff);
530     }
531     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
532     {
533         x = (SGD_PI / 2 + lat) * sin(lonDiff);
534         y = (SGD_PI / 2 + lat) * cos(lonDiff);
535     }
536     else
537     {
538         x = k * cos(lat) * sin(lonDiff);
539         y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
540     }
541
542     // flip for top-left origin
543     return QPointF(x, -y) * r;
544 }
545
546 SGGeod BaseDiagram::unproject(const QPointF& xy, const SGGeod& center)
547 {
548     double r = earth_radius_lat(center.getLatitudeRad());
549     double lat = 0,
550            lon = 0,
551            ref_lat = center.getLatitudeRad(),
552            ref_lon = center.getLongitudeRad(),
553            rho = QVector2D(xy).length(),
554            c = rho/r;
555
556     if (rho == 0) {
557         return center;
558     }
559
560     // invert y to balance the equivalent in project()
561     double x = xy.x(),
562             y = -xy.y();
563     lat = asin( cos(c) * sin(ref_lat) + (y * sin(c) * cos(ref_lat)) / rho);
564
565     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS)) // north pole
566     {
567         lon = ref_lon + atan(-x/y);
568     }
569     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS)) // south pole
570     {
571         lon = ref_lon + atan(x/y);
572     }
573     else
574     {
575         lon = ref_lon + atan(x* sin(c) / (rho * cos(ref_lat) * cos(c) - y * sin(ref_lat) * sin(c)));
576     }
577
578     return SGGeod::fromRad(lon, lat);
579 }
580
581 QPointF BaseDiagram::project(const SGGeod& geod) const
582 {
583     return project(geod, m_projectionCenter);
584 }
585
586 QPixmap BaseDiagram::iconForPositioned(const FGPositionedRef& pos,
587                                        const IconOptions& options)
588 {
589     // if airport type, check towered or untowered
590     bool small = options.testFlag(SmallIcons);
591
592     bool isTowered = false;
593     if (FGAirport::isAirportType(pos)) {
594         FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
595         isTowered = apt->hasTower();
596     }
597
598     switch (pos->type()) {
599     case FGPositioned::VOR:
600         if (static_cast<FGNavRecord*>(pos.ptr())->isVORTAC())
601             return QPixmap(":/vortac-icon");
602
603         if (static_cast<FGNavRecord*>(pos.ptr())->hasDME())
604             return QPixmap(":/vor-dme-icon");
605
606         return QPixmap(":/vor-icon");
607
608     case FGPositioned::AIRPORT:
609         return iconForAirport(static_cast<FGAirport*>(pos.ptr()), options);
610
611     case FGPositioned::HELIPORT:
612         return QPixmap(":/heliport-icon");
613     case FGPositioned::SEAPORT:
614         return QPixmap(isTowered ? ":/seaport-tower-icon" : ":/seaport-icon");
615     case FGPositioned::NDB:
616         return QPixmap(small ? ":/ndb-small-icon" : ":/ndb-icon");
617     case FGPositioned::FIX:
618         return QPixmap(":/waypoint-icon");
619
620     default:
621         break;
622     }
623
624     return QPixmap();
625 }
626
627 QPixmap BaseDiagram::iconForAirport(FGAirport* apt, const IconOptions& options)
628 {
629     if (apt->isClosed()) {
630         return QPixmap(":/airport-closed-icon");
631     }
632
633     if (!apt->hasHardRunwayOfLengthFt(1500)) {
634         return QPixmap(apt->hasTower() ? ":/airport-tower-icon" : ":/airport-icon");
635     }
636
637     if (options.testFlag(LargeAirportPlans) && apt->hasHardRunwayOfLengthFt(8500)) {
638         QPixmap result(32, 32);
639         result.fill(Qt::transparent);
640         {
641             QPainter p(&result);
642             p.setRenderHint(QPainter::Antialiasing, true);
643             QRectF b = result.rect().adjusted(4, 4, -4, -4);
644             QVector<QLineF> lines = projectAirportRuwaysIntoRect(apt, b);
645
646             p.setPen(QPen(QColor(0x03, 0x83, 0xbf), 8));
647             p.drawLines(lines);
648
649             p.setPen(QPen(Qt::white, 2));
650             p.drawLines(lines);
651         }
652         return result;
653     }
654
655     QPixmap result(25, 25);
656     result.fill(Qt::transparent);
657
658     {
659         QPainter p(&result);
660         p.setRenderHint(QPainter::Antialiasing, true);
661         p.setPen(Qt::NoPen);
662
663         p.setBrush(apt->hasTower() ? QColor(0x03, 0x83, 0xbf) :
664                                      QColor(0x9b, 0x5d, 0xa2));
665         p.drawEllipse(QPointF(13, 13), 10, 10);
666
667         FGRunwayRef r = apt->longestRunway();
668
669         p.setPen(QPen(Qt::white, 2));
670         p.translate(13, 13);
671         p.rotate(r->headingDeg());
672         p.drawLine(0, -8, 0, 8);
673     }
674
675     return result;
676 }
677
678 QVector<QLineF> BaseDiagram::projectAirportRuwaysWithCenter(FGAirportRef apt, const SGGeod& c)
679 {
680     QVector<QLineF> r;
681
682     const FGRunwayList& runways(apt->getRunwaysWithoutReciprocals());
683     FGRunwayList::const_iterator it;
684
685     for (it = runways.begin(); it != runways.end(); ++it) {
686         FGRunwayRef rwy = *it;
687         QPointF p1 = project(rwy->geod(), c);
688         QPointF p2 = project(rwy->end(), c);
689         r.append(QLineF(p1, p2));
690     }
691
692     return r;
693 }
694
695 void BaseDiagram::setAircraftType(LauncherAircraftType type)
696 {
697     m_aircraftType = type;
698     update();
699 }
700
701 QVector<QLineF> BaseDiagram::projectAirportRuwaysIntoRect(FGAirportRef apt, const QRectF &bounds)
702 {
703     QVector<QLineF> r = projectAirportRuwaysWithCenter(apt, apt->geod());
704
705     QRectF extent;
706     Q_FOREACH(const QLineF& l, r) {
707         extendRect(extent, l.p1());
708         extendRect(extent, l.p2());
709     }
710
711  // find constraining scale factor
712     double ratioInX = bounds.width() / extent.width();
713     double ratioInY = bounds.height() / extent.height();
714
715     QTransform t;
716     t.translate(bounds.left(), bounds.top());
717     t.scale(std::min(ratioInX, ratioInY),
718             std::min(ratioInX, ratioInY));
719     t.translate(-extent.left(), -extent.top()); // move unscaled to 0,0
720
721     for (int i=0; i<r.size(); ++i) {
722         r[i] = t.map(r[i]);
723     }
724
725     return r;
726 }