]> git.mxchange.org Git - flightgear.git/blob - src/GUI/BaseDiagram.cxx
1c938a0a0b51d8199e3354b4d7b20a9f9e9e9af6
[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 /* equatorial and polar earth radius */
35 const float rec  = 6378137;          // earth radius, equator (?)
36 const float rpol = 6356752.314f;      // earth radius, polar   (?)
37
38 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
39 static float earth_radius_lat( float lat )
40 {
41     double a = cos(lat)/rec;
42     double b = sin(lat)/rpol;
43     return 1.0f / sqrt( a * a + b * b );
44 }
45
46 BaseDiagram::BaseDiagram(QWidget* pr) :
47     QWidget(pr),
48     m_autoScalePan(true),
49     m_wheelAngleDeltaAccumulator(0)
50 {
51     setSizePolicy(QSizePolicy::MinimumExpanding,
52                   QSizePolicy::MinimumExpanding);
53     setMinimumSize(100, 100);
54 }
55
56 QTransform BaseDiagram::transform() const
57 {
58     QTransform t;
59     t.translate(width() / 2, height() / 2); // center projection origin in the widget
60     t.scale(m_scale, m_scale);
61
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());
66     return t;
67 }
68
69 void BaseDiagram::extendRect(QRectF &r, const QPointF &p)
70 {
71     if (p.x() < r.left()) {
72         r.setLeft(p.x());
73     } else if (p.x() > r.right()) {
74         r.setRight(p.x());
75     }
76
77     if (p.y() < r.top()) {
78         r.setTop(p.y());
79     } else if (p.y() > r.bottom()) {
80         r.setBottom(p.y());
81     }
82 }
83
84 void BaseDiagram::paintEvent(QPaintEvent* pe)
85 {
86     QPainter p(this);
87     p.setRenderHints(QPainter::Antialiasing);
88     p.fillRect(rect(), QColor(0x3f, 0x3f, 0x3f));
89
90     if (m_autoScalePan) {
91         // fit bounds within our available space, allowing for a margin
92         const int MARGIN = 32; // pixels
93         double ratioInX = (width() - MARGIN * 2) / m_bounds.width();
94         double ratioInY = (height() - MARGIN * 2) / m_bounds.height();
95         m_scale = std::min(ratioInX, ratioInY);
96     }
97
98     QTransform t(transform());
99     p.setTransform(t);
100
101     paintNavaids(&p);
102
103     paintContents(&p);
104 }
105
106 class MapFilter : public FGPositioned::TypeFilter
107 {
108 public:
109     MapFilter()
110     {
111       //  addType(FGPositioned::FIX);
112         addType(FGPositioned::AIRPORT);
113         addType(FGPositioned::NDB);
114         addType(FGPositioned::VOR);
115     }
116
117     virtual bool pass(FGPositioned* aPos) const
118     {
119         bool ok = TypeFilter::pass(aPos);
120         if (ok && (aPos->type() == FGPositioned::FIX)) {
121             // ignore fixes which end in digits
122             if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
123                 return false;
124             }
125         }
126
127         return ok;
128     }
129 };
130
131
132 void BaseDiagram::paintNavaids(QPainter* painter)
133 {
134     QTransform xf = painter->transform();
135     painter->setTransform(QTransform()); // reset to identity
136     QTransform invT = xf.inverted();
137     SGGeod topLeft = unproject(invT.map(QPointF(0,0)), m_projectionCenter);
138
139     double minRunwayLengthFt = (16 / m_scale) * SG_METER_TO_FEET;
140     // add 10nm fudge factor
141     double drawRangeNm = SGGeodesy::distanceNm(m_projectionCenter, topLeft) + 10.0;
142     //qDebug() << "draw range computed as:" << drawRangeNm;
143
144     MapFilter f;
145     FGPositionedList items = FGPositioned::findWithinRange(m_projectionCenter, drawRangeNm, &f);
146
147     // pass 0 - icons
148
149     FGPositionedList::const_iterator it;
150     for (it = items.begin(); it != items.end(); ++it) {
151         bool drawAsIcon = true;
152
153         if ((*it)->type() == FGPositioned::AIRPORT) {
154             FGAirport* apt = static_cast<FGAirport*>(it->ptr());
155             if (apt->hasHardRunwayOfLengthFt(minRunwayLengthFt)) {
156
157                 drawAsIcon = false;
158                 painter->setTransform(xf);
159                 QVector<QLineF> lines = projectAirportRuwaysWithCenter(apt, m_projectionCenter);
160
161                 QPen pen(QColor(0x03, 0x83, 0xbf), 8);
162                 pen.setCosmetic(true);
163                 painter->setPen(pen);
164                 painter->drawLines(lines);
165
166                 QPen linePen(Qt::white, 2);
167                 linePen.setCosmetic(true);
168                 painter->setPen(linePen);
169                 painter->drawLines(lines);
170
171                 painter->resetTransform();
172             }
173         }
174
175         if (drawAsIcon) {
176             QPixmap pm = iconForPositioned(*it);
177             QPointF loc = xf.map(project((*it)->geod()));
178             loc -= QPointF(pm.width() >> 1, pm.height() >> 1);
179             painter->drawPixmap(loc, pm);
180         }
181     }
182
183     // restore transform
184     painter->setTransform(xf);
185 }
186
187 void BaseDiagram::mousePressEvent(QMouseEvent *me)
188 {
189     m_lastMousePos = me->pos();
190     m_didPan = false;
191 }
192
193 void BaseDiagram::mouseMoveEvent(QMouseEvent *me)
194 {
195     m_autoScalePan = false;
196
197     QPointF delta = me->pos() - m_lastMousePos;
198     m_lastMousePos = me->pos();
199
200     // offset is stored in metres so we don't have to modify it when
201     // zooming
202     m_panOffset += (delta / m_scale);
203     m_didPan = true;
204
205     update();
206 }
207
208 int intSign(int v)
209 {
210     return (v == 0) ? 0 : (v < 0) ? -1 : 1;
211 }
212
213 void BaseDiagram::wheelEvent(QWheelEvent *we)
214 {
215     m_autoScalePan = false;
216
217     int delta = we->angleDelta().y();
218     if (delta == 0)
219         return;
220
221     if (intSign(m_wheelAngleDeltaAccumulator) != intSign(delta)) {
222         m_wheelAngleDeltaAccumulator = 0;
223     }
224
225     m_wheelAngleDeltaAccumulator += delta;
226     if (m_wheelAngleDeltaAccumulator > 120) {
227         m_wheelAngleDeltaAccumulator = 0;
228         m_scale *= 2.0;
229     } else if (m_wheelAngleDeltaAccumulator < -120) {
230         m_wheelAngleDeltaAccumulator = 0;
231         m_scale *= 0.5;
232     }
233
234     update();
235 }
236
237 void BaseDiagram::paintContents(QPainter* painter)
238 {
239 }
240
241 void BaseDiagram::recomputeBounds(bool resetZoom)
242 {
243     m_bounds = QRectF();
244     doComputeBounds();
245
246     if (resetZoom) {
247         m_autoScalePan = true;
248         m_scale = 1.0;
249         m_panOffset = QPointF();
250     }
251
252     update();
253 }
254
255 void BaseDiagram::doComputeBounds()
256 {
257     // no-op in the base class
258 }
259
260 void BaseDiagram::extendBounds(const QPointF& p)
261 {
262     extendRect(m_bounds, p);
263 }
264
265 QPointF BaseDiagram::project(const SGGeod& geod, const SGGeod& center)
266 {
267     double r = earth_radius_lat(geod.getLatitudeRad());
268     double ref_lat = center.getLatitudeRad(),
269     ref_lon = center.getLongitudeRad(),
270     lat = geod.getLatitudeRad(),
271     lon = geod.getLongitudeRad(),
272     lonDiff = lon - ref_lon;
273
274     double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
275     if (c == 0.0) {
276         // angular distance from center is 0
277         return QPointF(0.0, 0.0);
278     }
279
280     double k = c / sin(c);
281     double x, y;
282     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
283     {
284         x = (SGD_PI / 2 - lat) * sin(lonDiff);
285         y = -(SGD_PI / 2 - lat) * cos(lonDiff);
286     }
287     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
288     {
289         x = (SGD_PI / 2 + lat) * sin(lonDiff);
290         y = (SGD_PI / 2 + lat) * cos(lonDiff);
291     }
292     else
293     {
294         x = k * cos(lat) * sin(lonDiff);
295         y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
296     }
297
298     return QPointF(x, -y) * r;
299 }
300
301 SGGeod BaseDiagram::unproject(const QPointF& xy, const SGGeod& center)
302 {
303     double r = earth_radius_lat(center.getLatitudeRad());
304     double lat = 0,
305            lon = 0,
306            ref_lat = center.getLatitudeRad(),
307            ref_lon = center.getLongitudeRad(),
308            rho = QVector2D(xy).length(),
309            c = rho/r;
310
311     if (rho == 0) {
312         return center;
313     }
314
315     double x = xy.x(), y = xy.y();
316     lat = asin( cos(c) * sin(ref_lat) + (y * sin(c) * cos(ref_lat)) / rho);
317
318     if (ref_lat == (90 * SG_DEGREES_TO_RADIANS)) // north pole
319     {
320         lon = ref_lon + atan(-x/y);
321     }
322     else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS)) // south pole
323     {
324         lon = ref_lon + atan(x/y);
325     }
326     else
327     {
328         lon = ref_lon + atan(x* sin(c) / (rho * cos(ref_lat) * cos(c) - y * sin(ref_lat) * sin(c)));
329     }
330
331     return SGGeod::fromRad(lon, lat);
332 }
333
334 QPointF BaseDiagram::project(const SGGeod& geod) const
335 {
336     return project(geod, m_projectionCenter);
337 }
338
339 QPixmap BaseDiagram::iconForPositioned(const FGPositionedRef& pos)
340 {
341     // if airport type, check towered or untowered
342
343     bool isTowered = false;
344     if (FGAirport::isAirportType(pos)) {
345         FGAirport* apt = static_cast<FGAirport*>(pos.ptr());
346         isTowered = apt->hasTower();
347     }
348
349     switch (pos->type()) {
350     case FGPositioned::VOR:
351         // check for VORTAC
352
353         if (static_cast<FGNavRecord*>(pos.ptr())->hasDME())
354             return QPixmap(":/vor-dme-icon");
355
356         return QPixmap(":/vor-icon");
357
358     case FGPositioned::AIRPORT:
359         return iconForAirport(static_cast<FGAirport*>(pos.ptr()));
360
361     case FGPositioned::HELIPORT:
362         return QPixmap(":/heliport-icon");
363     case FGPositioned::SEAPORT:
364         return QPixmap(isTowered ? ":/seaport-tower-icon" : ":/seaport-icon");
365     case FGPositioned::NDB:
366         return QPixmap(":/ndb-icon");
367     case FGPositioned::FIX:
368         return QPixmap(":/waypoint-icon");
369
370     default:
371         break;
372     }
373
374     return QPixmap();
375 }
376
377 QPixmap BaseDiagram::iconForAirport(FGAirport* apt)
378 {
379     if (!apt->hasHardRunwayOfLengthFt(1500)) {
380         return QPixmap(apt->hasTower() ? ":/airport-tower-icon" : ":/airport-icon");
381     }
382
383     if (apt->hasHardRunwayOfLengthFt(8500)) {
384         QPixmap result(32, 32);
385         result.fill(Qt::transparent);
386         {
387             QPainter p(&result);
388             p.setRenderHint(QPainter::Antialiasing, true);
389             QRectF b = result.rect().adjusted(4, 4, -4, -4);
390             QVector<QLineF> lines = projectAirportRuwaysIntoRect(apt, b);
391
392             p.setPen(QPen(QColor(0x03, 0x83, 0xbf), 8));
393             p.drawLines(lines);
394
395             p.setPen(QPen(Qt::white, 2));
396             p.drawLines(lines);
397         }
398         return result;
399     }
400
401     QPixmap result(25, 25);
402     result.fill(Qt::transparent);
403
404     {
405         QPainter p(&result);
406         p.setRenderHint(QPainter::Antialiasing, true);
407         p.setPen(Qt::NoPen);
408
409         p.setBrush(apt->hasTower() ? QColor(0x03, 0x83, 0xbf) :
410                                      QColor(0x9b, 0x5d, 0xa2));
411         p.drawEllipse(QPointF(13, 13), 10, 10);
412
413         FGRunwayRef r = apt->longestRunway();
414
415         p.setPen(QPen(Qt::white, 2));
416         p.translate(13, 13);
417         p.rotate(r->headingDeg());
418         p.drawLine(0, -8, 0, 8);
419     }
420
421     return result;
422 }
423
424 QVector<QLineF> BaseDiagram::projectAirportRuwaysWithCenter(FGAirportRef apt, const SGGeod& c)
425 {
426     QVector<QLineF> r;
427
428     const FGRunwayList& runways(apt->getRunwaysWithoutReciprocals());
429     FGRunwayList::const_iterator it;
430
431     for (it = runways.begin(); it != runways.end(); ++it) {
432         FGRunwayRef rwy = *it;
433         QPointF p1 = project(rwy->geod(), c);
434         QPointF p2 = project(rwy->end(), c);
435         r.append(QLineF(p1, p2));
436     }
437
438     return r;
439 }
440
441 QVector<QLineF> BaseDiagram::projectAirportRuwaysIntoRect(FGAirportRef apt, const QRectF &bounds)
442 {
443     QVector<QLineF> r = projectAirportRuwaysWithCenter(apt, apt->geod());
444
445     QRectF extent;
446     Q_FOREACH(const QLineF& l, r) {
447         extendRect(extent, l.p1());
448         extendRect(extent, l.p2());
449     }
450
451  // find constraining scale factor
452     double ratioInX = bounds.width() / extent.width();
453     double ratioInY = bounds.height() / extent.height();
454
455     QTransform t;
456     t.translate(bounds.left(), bounds.top());
457     t.scale(std::min(ratioInX, ratioInY),
458             std::min(ratioInX, ratioInY));
459     t.translate(-extent.left(), -extent.top()); // move unscaled to 0,0
460
461     for (int i=0; i<r.size(); ++i) {
462         r[i] = t.map(r[i]);
463     }
464
465     return r;
466 }