]> git.mxchange.org Git - flightgear.git/blob - src/GUI/AirportDiagram.cxx
Launcher: airport diagram runways can be clicked
[flightgear.git] / src / GUI / AirportDiagram.cxx
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 #include <limits>
24
25 #include <QPainter>
26 #include <QDebug>
27 #include <QVector2D>
28 #include <QMouseEvent>
29
30 #include <Airports/airport.hxx>
31 #include <Airports/runways.hxx>
32 #include <Airports/parking.hxx>
33 #include <Airports/pavement.hxx>
34
35 /* equatorial and polar earth radius */
36 const float rec  = 6378137;          // earth radius, equator (?)
37 const float rpol = 6356752.314f;      // earth radius, polar   (?)
38
39 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
40 static float earth_radius_lat( float lat )
41 {
42     double a = cos(lat)/rec;
43     double b = sin(lat)/rpol;
44     return 1.0f / sqrt( a * a + b * b );
45 }
46
47 static double distanceToLineSegment(const QVector2D& p, const QVector2D& a,
48                                     const QVector2D& b, double* outT = NULL)
49 {
50     QVector2D ab(b - a);
51     QVector2D ac(p - a);
52     
53     // Squared length, to avoid a sqrt
54     const qreal len2 = ab.lengthSquared();
55     
56     // Line null, the projection can't exist, we return the first point
57     if (qIsNull(len2)) {
58         if (outT) {
59             *outT = 0.0;
60         }
61         return (p - a).length();
62     }
63     
64     // Parametric value of the projection on the line
65     const qreal t = (ac.x() * ab.x() + ac.y() * ab.y()) / len2;
66     
67     if (t < 0.0) {
68         // Point is before the first point
69         if (outT) {
70             *outT = 0.0;
71         }
72         return (p - a).length();
73     } else if (t > 1.0) {
74         // Point is after the second point
75         if (outT) {
76             *outT = 1.0;
77         }
78         return (p - b).length();
79     } else {
80         if (outT) {
81             *outT = t;
82         }
83         
84         const QVector2D proj = a + t * ab;
85         return (proj - p).length();
86     }
87     
88     return 0.0;
89 }
90
91 AirportDiagram::AirportDiagram(QWidget* pr) :
92 QWidget(pr)
93 {
94     setSizePolicy(QSizePolicy::MinimumExpanding,
95                   QSizePolicy::MinimumExpanding);
96     setMinimumSize(100, 100);
97 }
98
99 void AirportDiagram::setAirport(FGAirportRef apt)
100 {
101     m_airport = apt;
102     m_projectionCenter = apt ? apt->geod() : SGGeod();
103     m_scale = 1.0;
104     m_bounds = QRectF(); // clear
105     m_runways.clear();
106
107     if (apt) {
108         buildTaxiways();
109         buildPavements();
110     }
111
112     update();
113 }
114
115 FGRunwayRef AirportDiagram::selectedRunway() const
116 {
117     return m_selectedRunway;
118 }
119
120 void AirportDiagram::setSelectedRunway(FGRunwayRef r)
121 {
122     if (r == m_selectedRunway) {
123         return;
124     }
125     
126     m_selectedRunway = r;
127     update();
128 }
129
130 void AirportDiagram::addRunway(FGRunwayRef rwy)
131 {
132     Q_FOREACH(RunwayData rd, m_runways) {
133         if (rd.runway == rwy->reciprocalRunway()) {
134             return; // only add one end of reciprocal runways
135         }
136     }
137
138     QPointF p1 = project(rwy->geod()),
139     p2 = project(rwy->end());
140     extendBounds(p1);
141     extendBounds(p2);
142
143     RunwayData r;
144     r.p1 = p1;
145     r.p2 = p2;
146     r.widthM = qRound(rwy->widthM());
147     r.runway = rwy;
148     m_runways.append(r);
149     update();
150 }
151
152 void AirportDiagram::addParking(FGParking* park)
153 {
154     QPointF p = project(park->geod());
155     extendBounds(p);
156     update();
157 }
158
159 QTransform AirportDiagram::transform() const
160 {
161     // fit bounds within our available space, allowing for a margin
162     const int MARGIN = 32; // pixels
163     double ratioInX = (width() - MARGIN * 2) / m_bounds.width();
164     double ratioInY = (height() - MARGIN * 2) / m_bounds.height();
165     double scale = std::min(ratioInX, ratioInY);
166     
167     QTransform t;
168     t.translate(width() / 2, height() / 2); // center projection origin in the widget
169     t.scale(scale, scale);
170     // center the bounding box (may not be at the origin)
171     t.translate(-m_bounds.center().x(), -m_bounds.center().y());
172     return t;
173 }
174
175 void AirportDiagram::paintEvent(QPaintEvent* pe)
176 {
177     QPainter p(this);
178     p.setRenderHints(QPainter::Antialiasing);
179     p.fillRect(rect(), QColor(0x3f, 0x3f, 0x3f));
180
181     // fit bounds within our available space, allowing for a margin
182     const int MARGIN = 32; // pixels
183     double ratioInX = (width() - MARGIN * 2) / m_bounds.width();
184     double ratioInY = (height() - MARGIN * 2) / m_bounds.height();
185     double scale = std::min(ratioInX, ratioInY);
186
187     QTransform t(transform());
188     p.setTransform(t);
189
190 // pavements
191     QBrush brush(QColor(0x9f, 0x9f, 0x9f));
192     Q_FOREACH(const QPainterPath& path, m_pavements) {
193         p.drawPath(path);
194     }
195
196 // taxiways
197     Q_FOREACH(const TaxiwayData& t, m_taxiways) {
198         QPen pen(QColor(0x9f, 0x9f, 0x9f));
199         pen.setWidth(t.widthM);
200         p.setPen(pen);
201         p.drawLine(t.p1, t.p2);
202     }
203
204 // runways
205     QFont f;
206     f.setPixelSize(14);
207     p.setFont(f);
208
209     Q_FOREACH(const RunwayData& r, m_runways) {
210         QColor color(Qt::magenta);
211         if ((r.runway == m_selectedRunway) || (r.runway->reciprocalRunway() == m_selectedRunway)) {
212             color = Qt::yellow;
213         }
214         
215         p.setTransform(t);
216
217         QPen pen(color);
218         pen.setWidth(r.widthM);
219         p.setPen(pen);
220         
221         p.drawLine(r.p1, r.p2);
222
223     // draw idents
224         QString ident = QString::fromStdString(r.runway->ident());
225
226         p.translate(r.p1);
227         p.rotate(r.runway->headingDeg());
228         // invert scaling factor so we can use screen pixel sizes here
229         p.scale(1.0 / scale, 1.0/ scale);
230         
231         p.setPen((r.runway == m_selectedRunway) ? Qt::yellow : Qt::magenta);
232         p.drawText(QRect(-100, 5, 200, 200), ident, Qt::AlignHCenter | Qt::AlignTop);
233
234         FGRunway* recip = r.runway->reciprocalRunway();
235         QString recipIdent = QString::fromStdString(recip->ident());
236
237         p.setTransform(t);
238         p.translate(r.p2);
239         p.rotate(recip->headingDeg());
240         p.scale(1.0 / scale, 1.0/ scale);
241
242         p.setPen((r.runway->reciprocalRunway() == m_selectedRunway) ? Qt::yellow : Qt::magenta);
243         p.drawText(QRect(-100, 5, 200, 200), recipIdent, Qt::AlignHCenter | Qt::AlignTop);
244     }
245 }
246
247 void AirportDiagram::mouseReleaseEvent(QMouseEvent* me)
248 {
249     QTransform t(transform());
250     double minDist = std::numeric_limits<double>::max();
251     FGRunwayRef bestRunway;
252     
253     Q_FOREACH(const RunwayData& r, m_runways) {
254         QPointF p1(t.map(r.p1)), p2(t.map(r.p2));
255         double t;
256         double d = distanceToLineSegment(QVector2D(me->pos()),
257                                          QVector2D(p1),
258                                          QVector2D(p2), &t);
259         if (d < minDist) {
260             if (t > 0.5) {
261                 bestRunway = r.runway->reciprocalRunway();
262             } else {
263                 bestRunway = r.runway;
264             }
265             minDist = d;
266         }
267     }
268     
269     if (minDist < 16.0) {
270         emit clickedRunway(bestRunway);
271     }
272 }
273
274 void AirportDiagram::extendBounds(const QPointF& p)
275 {
276     if (p.x() < m_bounds.left()) {
277         m_bounds.setLeft(p.x());
278     } else if (p.x() > m_bounds.right()) {
279         m_bounds.setRight(p.x());
280     }
281
282     if (p.y() < m_bounds.top()) {
283         m_bounds.setTop(p.y());
284     } else if (p.y() > m_bounds.bottom()) {
285         m_bounds.setBottom(p.y());
286     }
287 }
288
289 QPointF AirportDiagram::project(const SGGeod& geod) const
290 {
291     double r = earth_radius_lat(geod.getLatitudeRad());
292     double ref_lat = m_projectionCenter.getLatitudeRad(),
293     ref_lon = m_projectionCenter.getLongitudeRad(),
294     lat = geod.getLatitudeRad(),
295     lon = geod.getLongitudeRad(),
296     lonDiff = lon - ref_lon;
297
298     double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
299     if (c == 0.0) {
300         // angular distance from center is 0
301         return QPointF(0.0, 0.0);
302     }
303
304     double k = c / sin(c);
305     double x, y;
306     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
307     {
308         x = (SGD_PI / 2 - lat) * sin(lonDiff);
309         y = -(SGD_PI / 2 - lat) * cos(lonDiff);
310     }
311     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
312     {
313         x = (SGD_PI / 2 + lat) * sin(lonDiff);
314         y = (SGD_PI / 2 + lat) * cos(lonDiff);
315     }
316     else
317     {
318         x = k * cos(lat) * sin(lonDiff);
319         y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
320     }
321
322     return QPointF(x, -y) * r * m_scale;
323 }
324
325 void AirportDiagram::buildTaxiways()
326 {
327     m_taxiways.clear();
328     for (unsigned int tIndex=0; tIndex < m_airport->numTaxiways(); ++tIndex) {
329         FGTaxiwayRef tx = m_airport->getTaxiwayByIndex(tIndex);
330
331         TaxiwayData td;
332         td.p1 = project(tx->geod());
333         td.p2 = project(tx->pointOnCenterline(tx->lengthM()));
334         extendBounds(td.p1);
335         extendBounds(td.p2);
336         td.widthM = tx->widthM();
337         m_taxiways.append(td);
338     }
339 }
340
341 void AirportDiagram::buildPavements()
342 {
343     m_pavements.clear();
344     for (unsigned int pIndex=0; pIndex < m_airport->numPavements(); ++pIndex) {
345         FGPavementRef pave = m_airport->getPavementByIndex(pIndex);
346         if (pave->getNodeList().empty()) {
347             continue;
348         }
349
350         QPainterPath pp;
351         QPointF startPoint;
352         bool closed = true;
353         QPointF p0 = project(pave->getNodeList().front()->mPos);
354
355         FGPavement::NodeList::const_iterator it;
356         for (it = pave->getNodeList().begin(); it != pave->getNodeList().end(); ) {
357             const FGPavement::BezierNode *bn = dynamic_cast<const FGPavement::BezierNode *>(it->get());
358             bool close = (*it)->mClose;
359
360             // increment iterator so we can look at the next point
361             ++it;
362             QPointF nextPoint = (it == pave->getNodeList().end()) ? startPoint : project((*it)->mPos);
363
364             if (bn) {
365                 QPointF control = project(bn->mControl);
366                 QPointF endPoint = close ? startPoint : nextPoint;
367                 pp.quadTo(control, endPoint);
368             } else {
369                 // straight line segment
370                 if (closed) {
371                     pp.moveTo(p0);
372                     closed = false;
373                     startPoint = p0;
374                 } else
375                     pp.lineTo(p0);
376             }
377
378             if (close) {
379                 closed = true;
380                 pp.closeSubpath();
381                 startPoint = QPointF();
382             }
383
384             p0 = nextPoint;
385         } // of nodes iteration
386
387         if (!closed) {
388             pp.closeSubpath();
389         }
390
391         m_pavements.append(pp);
392     } // of pavements iteration
393 }