FlightGear next
AirportDiagram.cxx
Go to the documentation of this file.
1// AirportDiagram.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 "AirportDiagram.hxx"
22
23
24#include <simgear/sg_inlines.h>
25
26#include <QPainter>
27#include <QDebug>
28#include <QVector2D>
29#include <QMouseEvent>
30
31#include <Airports/airport.hxx>
32#include <Airports/runways.hxx>
33#include <Airports/parking.hxx>
34#include <Airports/pavement.hxx>
36
37#include <Navaids/navrecord.hxx>
39
40#include "QmlPositioned.hxx"
41
42static double distanceToLineSegment(const QVector2D& p, const QVector2D& a,
43 const QVector2D& b, double* outT = NULL)
44{
45 QVector2D ab(b - a);
46 QVector2D ac(p - a);
47
48 // Squared length, to avoid a sqrt
49 const qreal len2 = ab.lengthSquared();
50
51 // Line null, the projection can't exist, we return the first point
52 if (qIsNull(len2)) {
53 if (outT) {
54 *outT = 0.0;
55 }
56 return (p - a).length();
57 }
58
59 // Parametric value of the projection on the line
60 const qreal t = (ac.x() * ab.x() + ac.y() * ab.y()) / len2;
61
62 if (t < 0.0) {
63 // Point is before the first point
64 if (outT) {
65 *outT = 0.0;
66 }
67 return (p - a).length();
68 } else if (t > 1.0) {
69 // Point is after the second point
70 if (outT) {
71 *outT = 1.0;
72 }
73 return (p - b).length();
74 } else {
75 if (outT) {
76 *outT = t;
77 }
78
79 const QVector2D proj = a + t * ab;
80 return (proj - p).length();
81 }
82}
83
84static double unitLengthAfterMapping(const QTransform& t)
85{
86 const QPointF tVec = t.map(QPointF(1.0, 0.0)) - t.map(QPointF(0.0, 0.0));
87 return QVector2D(tVec).length();
88}
89
91 BaseDiagram(pr)
92{
93 m_parkingIconPath.moveTo(0,0);
94 m_parkingIconPath.lineTo(-16, -16);
95 m_parkingIconPath.lineTo(-64, -16);
96 m_parkingIconPath.lineTo(-64, 16);
97 m_parkingIconPath.lineTo(-16, 16);
98 m_parkingIconPath.lineTo(0, 0);
99
100 m_parkingIconLeftPath.moveTo(0,0);
101 m_parkingIconLeftPath.lineTo(16, -16);
102 m_parkingIconLeftPath.lineTo(64, -16);
103 m_parkingIconLeftPath.lineTo(64, 16);
104 m_parkingIconLeftPath.lineTo(16, 16);
105 m_parkingIconLeftPath.lineTo(0, 0);
106
107 m_helipadBoundsPath.moveTo(0, 0);
108 m_helipadBoundsPath.addEllipse(QPointF(0, 0), 16.0, 16.0);
109
110 m_helipadIconPath.moveTo(0,0);
111 m_helipadIconPath.addEllipse(QPointF(0, 0), 16.0, 16.0);
112 m_helipadIconPath.addEllipse(QPointF(0, 0), 13.0, 13.0);
113
114 QFont f;
115 f.setPixelSize(24.0);
116 f.setBold(true);
117 QFontMetrics metrics(f);
118 qreal xOffset = metrics.horizontalAdvance("H") * 0.5;
119 qreal yOffset = metrics.capHeight() * 0.5;
120 m_helipadIconPath.addText(-xOffset, yOffset, f, "H");
121}
122
127
129{
130 m_airport = apt;
131 m_projectionCenter = apt ? apt->geod() : SGGeod();
132 m_runways.clear();
133 m_parking.clear();
134 m_helipads.clear();
135
136 if (apt) {
137 if (apt->type() == FGPositioned::HELIPORT) {
138 for (unsigned int r=0; r<apt->numHelipads(); ++r) {
139 FGHelipadRef pad = apt->getHelipadByIndex(r);
140 // add pad with index as data role
141 addHelipad(pad);
142 }
143 } else {
144 for (unsigned int r = 0; r < apt->numHelipads(); ++r) {
145 FGHelipadRef pad = apt->getHelipadByIndex(r);
146 addHelipad(pad);
147 }
148
149 for (unsigned int r=0; r<apt->numRunways(); ++r) {
150 addRunway(apt->getRunwayByIndex(r));
151 }
152 }
153
154 FGGroundNetwork* ground = apt->groundNetwork();
155 if (ground && ground->exists()) {
156 for (auto park : ground->allParkings()) {
157 addParking(park);
158 }
159 } // of was able to get ground-network
160
161 buildTaxiways();
162 buildPavements();
163 }
164
166 addIgnoredNavaid(apt);
167 recomputeBounds(true);
168 update();
169}
170
172{
173 if (pos && (m_selection == pos->inner())) {
174 return;
175 }
176
177 if (!pos) {
178 m_selection.clear();
179 } else {
180 m_selection = pos->inner();
181 }
182 emit selectionChanged();
183 recomputeBounds(false);
184 update();
185}
186
188{
189 if (m_approachDistance == distance) {
190 return;
191 }
192
193 m_approachDistance = distance;
194 recomputeBounds(false);
195 update();
197}
198
200{
201 return m_approachDistance;
202}
203
205{
206 if (!m_selection)
207 return nullptr;
208
209 return new QmlPositioned{m_selection};
210}
211
213{
214 if (!m_airport)
215 return 0;
216 return m_airport->guid();
217}
218
220{
221 if (guid == -1) {
222 m_airport.clear();
223 } else {
225 }
226 setAirport(m_airport);
227 emit airportChanged();
228}
229
231{
232 if (m_approachExtensionEnabled == e)
233 return;
234 m_approachExtensionEnabled = e;
235 recomputeBounds(true);
236 update();
238}
239
241{
242 Q_FOREACH(RunwayData rd, m_runways) {
243 if (rd.runway == rwy->reciprocalRunway()) {
244 return; // only add one end of reciprocal runways
245 }
246 }
247
248 RunwayData r;
249 r.p1 = project(rwy->geod());
250 r.p2 = project(rwy->end());
251 r.widthM = qRound(rwy->widthM());
252 r.runway = rwy;
253 m_runways.append(r);
254
255 extendBounds(r.p1);
256 extendBounds(r.p2);
257 update();
258}
259
261{
262 Q_FOREACH(const RunwayData& r, m_runways) {
263 extendBounds(r.p1);
264 extendBounds(r.p2);
265 }
266
267 Q_FOREACH(const TaxiwayData& t, m_taxiways) {
268 extendBounds(t.p1);
269 extendBounds(t.p2);
270 }
271
272 Q_FOREACH(const ParkingData& p, m_parking) {
273 extendBounds(p.pt, 10.0);
274 }
275
276 Q_FOREACH(const HelipadData& p, m_helipads) {
277 extendBounds(p.pt, 20.0);
278 }
279
280 FGRunway* runwaySelection = fgpositioned_cast<FGRunway>(m_selection);
281 if (runwaySelection && m_approachExtensionEnabled) {
282 double d = m_approachDistance.convertToUnit(Units::Kilometers).value * 1000;
283 QPointF pt = project(runwaySelection->pointOnCenterline(-d));
284 extendBounds(pt);
285 }
286}
287
289{
290 ParkingData pd = { project(park->geod()), park };
291 m_parking.push_back(pd);
292 extendBounds(pd.pt);
293 update();
294}
295
297{
298 HelipadData pd = { project(pad->geod()), pad };
299 m_helipads.push_back(pd);
300 extendBounds(pd.pt);
301 update();
302}
303
305{
306 QTransform t = p->transform();
307// pavements
308 QBrush brush(QColor(0x9f, 0x9f, 0x9f));
309 Q_FOREACH(const QPainterPath& path, m_pavements) {
310 p->drawPath(path);
311 }
312
313// taxiways
314 Q_FOREACH(const TaxiwayData& t, m_taxiways) {
315 QPen pen(QColor(0x9f, 0x9f, 0x9f));
316 pen.setWidth(t.widthM);
317 p->setPen(pen);
318 p->drawLine(t.p1, t.p2);
319 }
320
321 drawHelipads(p);
322 drawParkings(p);
323
324// runways
325 QFont f;
326 f.setPixelSize(14);
327 p->setFont(f);
328
329 // draw ILS first so underneath all runways
330 QPen pen(QColor(0x5f, 0x5f, 0x5f));
331 pen.setWidth(1);
332 pen.setCosmetic(true);
333 p->setPen(pen);
334
335 Q_FOREACH(const RunwayData& r, m_runways) {
336 drawILS(p, r.runway);
337 drawILS(p, r.runway->reciprocalRunway());
338 }
339
340 bool drawAircraft = false;
341 SGGeod aircraftPos;
342 int headingDeg;
343
344 FGRunway* runwaySelection = fgpositioned_cast<FGRunway>(m_selection);
345
346 // now draw the runways for real
347 Q_FOREACH(const RunwayData& r, m_runways) {
348
349 QColor color(Qt::magenta);
350 if ((r.runway == runwaySelection) || (r.runway->reciprocalRunway() == runwaySelection)) {
351 color = Qt::yellow;
352 }
353
354 p->setTransform(t);
355
356 QPen pen(color);
357 pen.setWidth(r.widthM);
358 p->setPen(pen);
359
360 p->drawLine(r.p1, r.p2);
361
362 // draw idents
363 QString ident = QString::fromStdString(r.runway->ident());
364
365 p->translate(r.p1);
366 p->rotate(r.runway->headingDeg());
367 // invert scaling factor so we can use screen pixel sizes here
368 p->scale(1.0 / m_scale, 1.0/ m_scale);
369
370 p->setPen((r.runway == runwaySelection) ? Qt::yellow : Qt::magenta);
371 p->drawText(QRect(-100, 5, 200, 200), ident, Qt::AlignHCenter | Qt::AlignTop);
372
373 FGRunway* recip = r.runway->reciprocalRunway();
374 QString recipIdent = QString::fromStdString(recip->ident());
375
376 p->setTransform(t);
377 p->translate(r.p2);
378 p->rotate(recip->headingDeg());
379 p->scale(1.0 / m_scale, 1.0/ m_scale);
380
381 p->setPen((r.runway->reciprocalRunway() == runwaySelection) ? Qt::yellow : Qt::magenta);
382 p->drawText(QRect(-100, 5, 200, 200), recipIdent, Qt::AlignHCenter | Qt::AlignTop);
383 }
384
385 if (runwaySelection) {
386 drawAircraft = true;
387 aircraftPos = runwaySelection->geod();
388 headingDeg = runwaySelection->headingDeg();
389 }
390
391 if (runwaySelection && m_approachExtensionEnabled) {
392 p->setTransform(t);
393 // draw approach extension point
394 double d = m_approachDistance.convertToUnit(Units::Kilometers).value * 1000;
395 QPointF pt = project(runwaySelection->pointOnCenterline(-d));
396 QPointF pt2 = project(runwaySelection->geod());
397 QPen pen(Qt::yellow);
398 pen.setWidth(2.0 / m_scale);
399 p->setPen(pen);
400 p->drawLine(pt, pt2);
401
402 aircraftPos = runwaySelection->pointOnCenterline(-d);
403 }
404
405 if (drawAircraft) {
406 p->setTransform(t);
407 paintAirplaneIcon(p, aircraftPos, headingDeg);
408 }
409
410 #if 0
411 p->resetTransform();
412 QPen testPen(Qt::cyan);
413 testPen.setWidth(1);
414 testPen.setCosmetic(true);
415 p->setPen(testPen);
416 p->setBrush(Qt::NoBrush);
417
418 double minWidth = 8.0 * unitLengthAfterMapping(t.inverted());
419
420 Q_FOREACH(const RunwayData& r, m_runways) {
421 QPainterPath pp = pathForRunway(r, t, minWidth);
422 p->drawPath(pp);
423 } // of runways iteration
424#endif
425}
426
427void AirportDiagram::drawHelipads(QPainter* painter)
428{
429 FGHelipad* selectedHelipad = fgpositioned_cast<FGHelipad>(m_selection);
430
431 Q_FOREACH(const HelipadData& p, m_helipads) {
432 painter->save();
433 painter->translate(p.pt);
434
435 if (p.helipad == selectedHelipad) {
436 painter->setBrush(Qt::yellow);
437 } else {
438 painter->setBrush(Qt::magenta);
439 }
440
441 painter->drawPath(m_helipadIconPath);
442 painter->restore();
443 }
444}
445
446void AirportDiagram::drawParking(QPainter* painter, const ParkingData& p) const
447{
448 painter->save();
449 painter->translate(p.pt);
450
451 double hdg = p.parking->getHeading();
452 bool useLeftIcon = false;
453 QRect labelRect(-62, -14, 40, 28);
454
455 if (hdg > 180.0) {
456 hdg += 90;
457 useLeftIcon = true;
458 labelRect = QRect(22, -14, 40, 28);
459 } else {
460 hdg -= 90;
461 }
462
463 painter->rotate(hdg);
464 painter->setPen(Qt::NoPen);
465
466 FGParking* selectedParking = fgpositioned_cast<FGParking>(m_selection);
467 if (p.parking == selectedParking) {
468 painter->setBrush(Qt::yellow);
469 } else {
470 painter->setBrush(QColor(255, 196, 196)); // kind of pink
471 }
472
473 painter->drawPath(useLeftIcon ? m_parkingIconLeftPath : m_parkingIconPath);
474
475 // ensure the selection colour is quite visible, by not filling
476 // with white when selected
477 if (p.parking != selectedParking) {
478 painter->fillRect(labelRect, Qt::white);
479 }
480
481 QFont f = painter->font();
482 f.setPixelSize(20);
483 painter->setFont(f);
484
485 QString parkingName = QString::fromStdString(p.parking->name());
486 int textFlags = Qt::AlignVCenter | Qt::AlignHCenter | Qt::TextWordWrap;
487 QRectF bounds = painter->boundingRect(labelRect, textFlags, parkingName);
488 if (bounds.height() > labelRect.height()) {
489 f.setPixelSize(10);
490 painter->setFont(f);
491 }
492
493 // draw text
494 painter->setPen(Qt::black);
495 painter->drawText(labelRect, textFlags, parkingName);
496 painter->restore();
497}
498
499AirportDiagram::ParkingData AirportDiagram::findParkingData(const FGParkingRef &pk) const
500{
501 FGParking* selectedParking = fgpositioned_cast<FGParking>(m_selection);
502 if (!selectedParking)
503 return {};
504
505 Q_FOREACH(const ParkingData& p, m_parking) {
506 if (p.parking == selectedParking) {
507 return p;
508 }
509 }
510
511 return {};
512}
513
514void AirportDiagram::drawParkings(QPainter* painter) const
515{
516 FGParking* selectedParking = fgpositioned_cast<FGParking>(m_selection);
517
518 Q_FOREACH(const ParkingData& p, m_parking) {
519 if (p.parking == selectedParking) {
520 continue; // skip and draw last
521 }
522
523 drawParking(painter, p);
524 }
525
526 if (selectedParking) {
527 drawParking(painter, findParkingData(selectedParking));
528 }
529}
530
531void AirportDiagram::drawILS(QPainter* painter, FGRunwayRef runway) const
532{
533 if (!runway)
534 return;
535
536 FGNavRecord* loc = runway->ILS();
537 if (!loc)
538 return;
539
540 double halfBeamWidth = loc->localizerWidth() * 0.5;
541 QPointF threshold = project(runway->threshold());
542 double rangeM = loc->get_range() * SG_NM_TO_METER;
543 double radial = loc->get_multiuse();
544 SG_NORMALIZE_RANGE(radial, 0.0, 360.0);
545
546// compute the three end points at the wide end of the arrow
547 QPointF endCentre = project(SGGeodesy::direct(loc->geod(), radial, -rangeM));
548 QPointF endR = project(SGGeodesy::direct(loc->geod(), radial + halfBeamWidth, -rangeM * 1.1));
549 QPointF endL = project(SGGeodesy::direct(loc->geod(), radial - halfBeamWidth, -rangeM * 1.1));
550
551 painter->drawLine(threshold, endCentre);
552 painter->drawLine(threshold, endL);
553 painter->drawLine(threshold, endR);
554 painter->drawLine(endL, endCentre);
555 painter->drawLine(endR, endCentre);
556}
557
559{
560 me->accept();
561 QTransform t(transform());
562 double minWidth = 8.0 * unitLengthAfterMapping(t.inverted());
563
564 Q_FOREACH (const HelipadData& pad, m_helipads) {
565 QPainterPath pp = pathForHelipad(pad, t);
566 //imgPaint.drawPath(pp);
567 if (pp.contains(me->pos())) {
568 emit clicked(new QmlPositioned{pad.helipad});
569 return;
570 }
571 }
572
573 Q_FOREACH(const RunwayData& r, m_runways) {
574 QPainterPath pp = pathForRunway(r, t, minWidth);
575 if (pp.contains(me->pos())) {
576 // check which end was clicked
577 QPointF p1(t.map(r.p1)), p2(t.map(r.p2));
578 double param;
579 distanceToLineSegment(QVector2D(me->pos()), QVector2D(p1), QVector2D(p2), &param);
580 const FGRunwayRef clickedRunway = (param > 0.5) ? FGRunwayRef{r.runway->reciprocalRunway()} : r.runway;
581 emit clicked(new QmlPositioned{clickedRunway});
582 return;
583 }
584 } // of runways iteration
585
586 Q_FOREACH(const ParkingData& parking, m_parking) {
587 QPainterPath pp = pathForParking(parking, t);
588 if (pp.contains(me->pos())) {
589 emit clicked(new QmlPositioned{parking.parking});
590 return;
591 }
592 }
593}
594
595QPainterPath AirportDiagram::pathForRunway(const RunwayData& r, const QTransform& t,
596 const double minWidth) const
597{
598 QPainterPath pp;
599 double width = qMax(static_cast<double>(r.widthM), minWidth);
600 double halfWidth = width * 0.5;
601 QVector2D v = QVector2D(r.p2 - r.p1);
602 v.normalize();
603 QVector2D halfVec = QVector2D(v.y(), -v.x()) * halfWidth;
604
605 pp.moveTo(r.p1 - halfVec.toPointF());
606 pp.lineTo(r.p1 + halfVec.toPointF());
607 pp.lineTo(r.p2 + halfVec.toPointF());
608 pp.lineTo(r.p2 - halfVec.toPointF());
609 pp.closeSubpath();
610
611 return t.map(pp);
612}
613
614QPainterPath AirportDiagram::pathForParking(const ParkingData& p, const QTransform& t) const
615{
616 bool useLeftIcon = false;
617 double hdg = p.parking->getHeading();
618
619 if (hdg > 180.0) {
620 hdg += 90;
621 useLeftIcon = true;
622 } else {
623 hdg -= 90;
624 }
625
626 QTransform x = t;
627 x.translate(p.pt.x(), p.pt.y());
628 x.rotate(hdg);
629 return x.map(useLeftIcon ? m_parkingIconLeftPath : m_parkingIconPath);
630}
631
632QPainterPath AirportDiagram::pathForHelipad(const HelipadData& h, const QTransform& t) const
633{
634 QTransform x = t;
635 x.translate(h.pt.x(), h.pt.y());
636 return x.map(m_helipadBoundsPath);
637}
638
639void AirportDiagram::buildTaxiways()
640{
641 m_taxiways.clear();
642 for (unsigned int tIndex=0; tIndex < m_airport->numTaxiways(); ++tIndex) {
643 FGTaxiwayRef tx = m_airport->getTaxiwayByIndex(tIndex);
644
645 TaxiwayData td;
646 td.p1 = project(tx->geod());
647 td.p2 = project(tx->pointOnCenterline(tx->lengthM()));
648
649 td.widthM = tx->widthM();
650 m_taxiways.append(td);
651 }
652}
653
654void AirportDiagram::buildPavements()
655{
656 m_pavements.clear();
657 auto pavementlist = m_airport->getPavements();
658 for (auto pvtiter = pavementlist.begin(); pvtiter != pavementlist.end(); ++pvtiter)
659 {
660 FGPavementRef pave = *pvtiter;
661 if (pave->getNodeList().empty()) {
662 continue;
663 }
664
665 QPainterPath pp;
666 QPointF startPoint;
667 bool closed = true;
668 QPointF p0 = project(pave->getNodeList().front()->mPos);
669
670 FGPavement::NodeList::const_iterator it;
671 for (it = pave->getNodeList().begin(); it != pave->getNodeList().end(); ) {
672 const FGPavement::BezierNode *bn = dynamic_cast<const FGPavement::BezierNode *>(it->get());
673 bool close = (*it)->mClose;
674
675 // increment iterator so we can look at the next point
676 ++it;
677 QPointF nextPoint = (it == pave->getNodeList().end()) ? startPoint : project((*it)->mPos);
678
679 if (bn) {
680 QPointF control = project(bn->mControl);
681 QPointF endPoint = close ? startPoint : nextPoint;
682 pp.quadTo(control, endPoint);
683 } else {
684 // straight line segment
685 if (closed) {
686 pp.moveTo(p0);
687 closed = false;
688 startPoint = p0;
689 } else
690 pp.lineTo(p0);
691 }
692
693 if (close) {
694 closed = true;
695 pp.closeSubpath();
696 startPoint = QPointF();
697 }
698
699 p0 = nextPoint;
700 } // of nodes iteration
701
702 if (!closed) {
703 pp.closeSubpath();
704 }
705
706 m_pavements.append(pp);
707 } // of pavements iteration
708}
static double distanceToLineSegment(const QVector2D &p, const QVector2D &a, const QVector2D &b, double *outT=NULL)
static double unitLengthAfterMapping(const QTransform &t)
#define p2(x, y)
#define p(x)
SGSharedPtr< FGTaxiway > FGTaxiwayRef
SGSharedPtr< FGHelipad > FGHelipadRef
SGSharedPtr< FGPavement > FGPavementRef
SGSharedPtr< FGAirport > FGAirportRef
SGSharedPtr< FGRunway > FGRunwayRef
SGSharedPtr< FGParking > FGParkingRef
void airportChanged()
void setApproachExtension(QuantityValue distance)
void clicked(QmlPositioned *pos)
void selectionChanged()
void paintContents(QPainter *) override
void setApproachExtensionEnabled(bool e)
void doComputeBounds() override
qlonglong airportGuid() const
void setAirportGuid(qlonglong guid)
void addParking(FGParkingRef park)
QuantityValue approachExtension
void addRunway(FGRunwayRef rwy)
AirportDiagram(QQuickItem *pr=nullptr)
void approachExtensionChanged()
virtual ~AirportDiagram()
void setAirport(FGAirportRef apt)
void mouseReleaseEvent(QMouseEvent *me) override
void setSelection(QmlPositioned *pos)
QmlPositioned * selection
void addHelipad(FGHelipadRef pad)
BaseDiagram(QQuickItem *pr=nullptr)
void clearIgnoredNavaids()
QTransform transform() const
void paintAirplaneIcon(QPainter *painter, const SGGeod &geod, int headingDeg)
SGGeod m_projectionCenter
void extendBounds(const QPointF &p, double radiusM=1.0)
void addIgnoredNavaid(FGPositionedRef pos)
QPointF project(const SGGeod &geod) const
void recomputeBounds(bool resetZoom)
const FGParkingList & allParkings() const
int get_range() const
Definition navrecord.hxx:77
double get_multiuse() const
Definition navrecord.hxx:78
double localizerWidth() const
return the localizer width, in degrees computation is based up ICAO stdandard width at the runway thr...
Definition navrecord.cxx:63
virtual const SGGeod & geod() const
const std::string & ident() const
double headingDeg() const
Runway heading in degrees.
SGGeod pointOnCenterline(double aOffset) const
Retrieve a position on the extended centerline.
FGPositionedRef inner() const
@ Kilometers
static NavDataCache * instance()
T * fgpositioned_cast(FGPositioned *p)