5 #include "MapWidget.hxx"
8 #include <algorithm> // for std::sort
9 #include <plib/puAux.h>
11 #include <simgear/sg_inlines.h>
12 #include <simgear/route/waypoint.hxx>
13 #include <simgear/sg_inlines.h>
14 #include <simgear/misc/strutils.hxx>
15 #include <simgear/magvar/magvar.hxx>
16 #include <simgear/timing/sg_time.hxx> // for magVar julianDate
17 #include <simgear/structure/exception.hxx>
19 #include <Main/globals.hxx>
20 #include <Main/fg_props.hxx>
21 #include <Autopilot/route_mgr.hxx>
22 #include <Navaids/positioned.hxx>
23 #include <Navaids/navrecord.hxx>
24 #include <Navaids/navlist.hxx>
25 #include <Navaids/fix.hxx>
26 #include <Airports/simple.hxx>
27 #include <Airports/runways.hxx>
28 #include <Main/fg_os.hxx> // fgGetKeyModifiers()
29 #include <Navaids/routePath.hxx>
31 const char* RULER_LEGEND_KEY = "ruler-legend";
33 /* equatorial and polar earth radius */
34 const float rec = 6378137; // earth radius, equator (?)
35 const float rpol = 6356752.314f; // earth radius, polar (?)
37 /************************************************************************
38 some trigonometric helper functions
39 (translated more or less directly from Alexei Novikovs perl original)
40 *************************************************************************/
42 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
43 static float earth_radius_lat( float lat )
45 double a = cos(lat)/rec;
46 double b = sin(lat)/rpol;
47 return 1.0f / sqrt( a * a + b * b );
50 ///////////////////////////////////////////////////////////////////////////
52 static puBox makePuBox(int x, int y, int w, int h)
62 static bool puBoxIntersect(const puBox& a, const puBox& b)
64 int x0 = SG_MAX2(a.min[0], b.min[0]);
65 int y0 = SG_MAX2(a.min[1], b.min[1]);
66 int x1 = SG_MIN2(a.max[0], b.max[0]);
67 int y1 = SG_MIN2(a.max[1], b.max[1]);
69 return (x0 <= x1) && (y0 <= y1);
73 typedef std::vector<MapData*> MapDataVec;
78 static const int HALIGN_LEFT = 1;
79 static const int HALIGN_CENTER = 2;
80 static const int HALIGN_RIGHT = 3;
82 static const int VALIGN_TOP = 1 << 4;
83 static const int VALIGN_CENTER = 2 << 4;
84 static const int VALIGN_BOTTOM = 3 << 4;
86 MapData(int priority) :
92 _offsetDir(HALIGN_LEFT | VALIGN_CENTER),
98 void setLabel(const std::string& label)
100 if (label == _label) {
101 return; // common case, and saves invalidation
108 void setText(const std::string &text)
110 if (_rawText == text) {
111 return; // common case, and saves invalidation
118 void setDataVisible(bool vis) {
119 if (vis == _dataVisible) {
123 if (_rawText.empty()) {
131 static void setFont(puFont f)
134 _fontHeight = f.getStringHeight();
135 _fontDescender = f.getStringDescender();
138 static void setPalette(puColor* pal)
143 void setPriority(int pri)
149 { return _priority; }
151 void setAnchor(const SGVec2d& anchor)
156 void setOffset(int direction, int px)
158 if ((_offsetPx == px) && (_offsetDir == direction)) {
163 _offsetDir = direction;
167 bool isClipped(const puBox& vis) const
170 if ((_width < 1) || (_height < 1)) {
174 return !puBoxIntersect(vis, box());
177 bool overlaps(const MapDataVec& l) const
182 MapDataVec::const_iterator it;
183 for (it = l.begin(); it != l.end(); ++it) {
184 if (puBoxIntersect(b, (*it)->box())) {
187 } // of list iteration
196 _anchor.x() + _offset.x(),
197 _anchor.y() + _offset.y(),
205 int xx = _anchor.x() + _offset.x();
206 int yy = _anchor.y() + _offset.y();
209 puBox box(makePuBox(0,0,_width, _height));
211 box.draw(xx, yy, PUSTYLE_DROPSHADOW, _palette, FALSE, border);
214 int lineHeight = _fontHeight;
215 int xPos = xx + MARGIN;
216 int yPos = yy + _height - (lineHeight + MARGIN);
217 glColor3f(0.8, 0.8, 0.8);
219 for (unsigned int ln=0; ln<_lines.size(); ++ln) {
220 _font.drawString(_lines[ln].c_str(), xPos, yPos);
221 yPos -= lineHeight + LINE_LEADING;
224 glColor3f(0.8, 0.8, 0.8);
225 _font.drawString(_label.c_str(), xx, yy + _fontDescender);
239 bool isExpired() const
240 { return (_age > 100); }
242 static bool order(MapData* a, MapData* b)
244 return a->_priority > b->_priority;
247 void validate() const
267 void measureData() const
269 _lines = simgear::strutils::split(_rawText, "\n");
270 // measure text to find width and height
274 for (unsigned int ln=0; ln<_lines.size(); ++ln) {
275 _height += _fontHeight;
277 _height += LINE_LEADING;
280 int lw = _font.getStringWidth(_lines[ln].c_str());
281 _width = std::max(_width, lw);
282 } // of line measurement
284 if ((_width < 1) || (_height < 1)) {
289 _height += MARGIN * 2;
290 _width += MARGIN * 2;
293 void measureLabel() const
295 if (_label.empty()) {
296 _width = _height = -1;
300 _height = _fontHeight;
301 _width = _font.getStringWidth(_label.c_str());
304 void computeOffset() const
306 _dirtyOffset = false;
307 if ((_width <= 0) || (_height <= 0)) {
314 switch (_offsetDir & 0x0f) {
321 hOffset = -(_width>>1);
325 hOffset = -(_offsetPx + _width);
329 switch (_offsetDir & 0xf0) {
332 vOffset = -(_offsetPx + _height);
336 vOffset = -(_height>>1);
344 _offset = SGVec2d(hOffset, vOffset);
347 static const int LINE_LEADING = 3;
348 static const int MARGIN = 3;
350 mutable bool _dirtyText;
351 mutable bool _dirtyOffset;
353 std::string _rawText;
355 mutable std::vector<std::string> _lines;
357 mutable int _width, _height;
361 mutable SGVec2d _offset;
365 static puColor* _palette;
366 static int _fontHeight;
367 static int _fontDescender;
370 puFont MapData::_font;
371 puColor* MapData::_palette;
372 int MapData::_fontHeight = 0;
373 int MapData::_fontDescender = 0;
375 ///////////////////////////////////////////////////////////////////////////
377 const int MAX_ZOOM = 12;
378 const int SHOW_DETAIL_ZOOM = 8;
379 const int CURSOR_PAN_STEP = 32;
381 MapWidget::MapWidget(int x, int y, int maxX, int maxY) :
382 puObject(x,y,maxX, maxY)
384 _route = static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"));
385 _gps = fgGetNode("/instrumentation/gps");
390 _orthoAzimuthProject = false;
392 MapData::setFont(legendFont);
393 MapData::setPalette(colour);
395 _magVar = new SGMagVar();
398 MapWidget::~MapWidget()
404 void MapWidget::setProperty(SGPropertyNode_ptr prop)
407 int zoom = _root->getIntValue("zoom", -1);
409 _root->setIntValue("zoom", 6); // default zoom
412 // expose MAX_ZOOM to the UI
413 _root->setIntValue("max-zoom", MAX_ZOOM);
414 _root->setBoolValue("centre-on-aircraft", true);
415 _root->setBoolValue("draw-data", false);
416 _root->setBoolValue("magnetic-headings", true);
419 void MapWidget::setSize(int w, int h)
421 puObject::setSize(w, h);
428 void MapWidget::doHit( int button, int updown, int x, int y )
430 puObject::doHit(button, updown, x, y);
431 if (updown == PU_DRAG) {
436 if (button == 3) { // mouse-wheel up
438 } else if (button == 4) { // mouse-wheel down
442 if (button != active_mouse_button) {
446 _hitLocation = SGVec2d(x - abox.min[0], y - abox.min[1]);
448 if (updown == PU_UP) {
449 puDeactivateWidget();
450 } else if (updown == PU_DOWN) {
451 puSetActiveWidget(this, x, y);
453 if (fgGetKeyModifiers() & KEYMOD_CTRL) {
454 _clickGeod = unproject(_hitLocation - SGVec2d(_width>>1, _height>>1));
459 void MapWidget::handlePan(int x, int y)
461 SGVec2d delta = SGVec2d(x, y) - _hitLocation;
463 _hitLocation = SGVec2d(x,y);
466 int MapWidget::checkKey (int key, int updown )
468 if ((updown == PU_UP) || !isVisible () || !isActive () || (window != puGetWindow())) {
476 pan(SGVec2d(0, -CURSOR_PAN_STEP));
480 pan(SGVec2d(0, CURSOR_PAN_STEP));
484 pan(SGVec2d(CURSOR_PAN_STEP, 0));
488 pan(SGVec2d(-CURSOR_PAN_STEP, 0));
507 void MapWidget::pan(const SGVec2d& delta)
510 _projectionCenter = unproject(-delta);
513 int MapWidget::zoom() const
515 int z = _root->getIntValue("zoom");
516 SG_CLAMP_RANGE(z, 0, MAX_ZOOM);
520 void MapWidget::zoomIn()
522 if (zoom() >= MAX_ZOOM) {
526 _root->setIntValue("zoom", zoom() + 1);
529 void MapWidget::zoomOut()
535 _root->setIntValue("zoom", zoom() - 1);
538 void MapWidget::draw(int dx, int dy)
540 _aircraft = SGGeod::fromDeg(fgGetDouble("/position/longitude-deg"),
541 fgGetDouble("/position/latitude-deg"));
543 bool mag = _root->getBoolValue("magnetic-headings");
544 if (mag != _magneticHeadings) {
545 clearData(); // flush cached data text, since it often includes heading
546 _magneticHeadings = mag;
550 _root->setBoolValue("centre-on-aircraft", false);
553 else if (_root->getBoolValue("centre-on-aircraft")) {
554 _projectionCenter = _aircraft;
557 double julianDate = globals->get_time_params()->getJD();
558 _magVar->update(_projectionCenter, julianDate);
560 bool aircraftUp = _root->getBoolValue("aircraft-heading-up");
562 _upHeading = fgGetDouble("/orientation/heading-deg");
567 _cachedZoom = MAX_ZOOM - zoom();
568 SGGeod topLeft = unproject(SGVec2d(_width/2, _height/2));
569 // compute draw range, including a fudge factor for ILSs and other 'long'
571 _drawRangeNm = SGGeodesy::distanceNm(_projectionCenter, topLeft) + 10.0;
573 // drawing operations
574 GLint sx = (int) abox.min[0],
575 sy = (int) abox.min[1];
576 glScissor(dx + sx, dy + sy, _width, _height);
577 glEnable(GL_SCISSOR_TEST);
579 glMatrixMode(GL_MODELVIEW);
581 // cetere drawing about the widget center (which is also the
582 // projection centre)
583 glTranslated(dx + sx + (_width/2), dy + sy + (_height/2), 0.0);
588 int textHeight = legendFont.getStringHeight() + 5;
591 SGVec2d loc = project(_aircraft);
592 glColor3f(1.0, 1.0, 1.0);
593 drawLine(loc, SGVec2d(loc.x(), (_height / 2) - textHeight));
596 if (_magneticHeadings) {
597 displayHdg = (int) fgGetDouble("/orientation/heading-magnetic-deg");
599 displayHdg = (int) _upHeading;
602 double y = (_height / 2) - textHeight;
604 ::snprintf(buf, 16, "%d", displayHdg);
605 int sw = legendFont.getStringWidth(buf);
606 legendFont.drawString(buf, loc.x() - sw/2, y);
613 drawNavRadio(fgGetNode("/instrumentation/nav[0]", false));
614 drawNavRadio(fgGetNode("/instrumentation/nav[1]", false));
615 paintAircraftLocation(_aircraft);
622 glDisable(GL_SCISSOR_TEST);
625 void MapWidget::paintRuler()
627 if (_clickGeod == SGGeod()) {
631 SGVec2d acftPos = project(_aircraft);
632 SGVec2d clickPos = project(_clickGeod);
634 glColor4f(0.0, 1.0, 1.0, 0.6);
635 drawLine(acftPos, clickPos);
637 circleAtAlt(clickPos, 8, 10, 5);
639 double dist, az, az2;
640 SGGeodesy::inverse(_aircraft, _clickGeod, az, az2, dist);
642 ::snprintf(buffer, 1024, "%03d/%.1fnm",
643 displayHeading(az), dist * SG_METER_TO_NM);
645 MapData* d = getOrCreateDataForKey((void*) RULER_LEGEND_KEY);
647 d->setAnchor(clickPos);
648 d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
649 d->setPriority(20000);
654 void MapWidget::paintAircraftLocation(const SGGeod& aircraftPos)
656 SGVec2d loc = project(aircraftPos);
658 double hdg = fgGetDouble("/orientation/heading-deg");
661 glColor4f(1.0, 1.0, 0.0, 1.0);
663 glTranslated(loc.x(), loc.y(), 0.0);
664 glRotatef(hdg - _upHeading, 0.0, 0.0, -1.0);
666 const SGVec2d wingspan(12, 0);
667 const SGVec2d nose(0, 8);
668 const SGVec2d tail(0, -14);
669 const SGVec2d tailspan(4,0);
671 drawLine(-wingspan, wingspan);
672 drawLine(nose, tail);
673 drawLine(tail - tailspan, tail + tailspan);
679 void MapWidget::paintRoute()
681 if (_route->numWaypts() < 2) {
685 RoutePath path(_route->waypts());
687 // first pass, draw the actual lines
690 for (int w=0; w<_route->numWaypts(); ++w) {
691 SGGeodVec gv(path.pathForIndex(w));
696 if (w < _route->currentIndex()) {
697 glColor4f(0.5, 0.5, 0.5, 0.7);
699 glColor4f(1.0, 0.0, 1.0, 1.0);
702 flightgear::WayptRef wpt(_route->wayptAtIndex(w));
703 if (wpt->flag(flightgear::WPT_MISS)) {
704 glEnable(GL_LINE_STIPPLE);
705 glLineStipple(1, 0x00FF);
708 glBegin(GL_LINE_STRIP);
709 for (unsigned int i=0; i<gv.size(); ++i) {
710 SGVec2d p = project(gv[i]);
711 glVertex2d(p.x(), p.y());
715 glDisable(GL_LINE_STIPPLE);
719 // second pass, draw waypoint symbols and data
720 for (int w=0; w < _route->numWaypts(); ++w) {
721 flightgear::WayptRef wpt(_route->wayptAtIndex(w));
722 SGGeod g = path.positionForIndex(w);
724 continue; // Vectors or similar
727 SGVec2d p = project(g);
728 glColor4f(1.0, 0.0, 1.0, 1.0);
729 circleAtAlt(p, 8, 12, 5);
731 std::ostringstream legend;
732 legend << wpt->ident();
733 if (wpt->altitudeRestriction() != flightgear::RESTRICT_NONE) {
734 legend << '\n' << SGMiscd::roundToInt(wpt->altitudeFt()) << '\'';
737 if (wpt->speedRestriction() == flightgear::SPEED_RESTRICT_MACH) {
738 legend << '\n' << wpt->speedMach() << "M";
739 } else if (wpt->speedRestriction() != flightgear::RESTRICT_NONE) {
740 legend << '\n' << SGMiscd::roundToInt(wpt->speedKts()) << "Kts";
743 MapData* d = getOrCreateDataForKey(reinterpret_cast<void*>(w * 2));
744 d->setText(legend.str());
745 d->setLabel(wpt->ident());
747 d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
748 d->setPriority(w < _route->currentIndex() ? 9000 : 12000);
750 } // of second waypoint iteration
754 * Round a SGGeod to an arbitrary precision.
755 * For example, passing precision of 0.5 will round to the nearest 0.5 of
756 * a degree in both lat and lon - passing in 3.0 rounds to the nearest 3 degree
757 * multiple, and so on.
759 static SGGeod roundGeod(double precision, const SGGeod& g)
761 double lon = SGMiscd::round(g.getLongitudeDeg() / precision);
762 double lat = SGMiscd::round(g.getLatitudeDeg() / precision);
764 return SGGeod::fromDeg(lon * precision, lat * precision);
767 bool MapWidget::drawLineClipped(const SGVec2d& a, const SGVec2d& b)
769 double minX = SGMiscd::min(a.x(), b.x()),
770 minY = SGMiscd::min(a.y(), b.y()),
771 maxX = SGMiscd::max(a.x(), b.x()),
772 maxY = SGMiscd::max(a.y(), b.y());
774 int hh = _height >> 1, hw = _width >> 1;
776 if ((maxX < -hw) || (minX > hw) || (minY > hh) || (maxY < -hh)) {
780 glVertex2dv(a.data());
781 glVertex2dv(b.data());
785 SGVec2d MapWidget::gridPoint(int ix, int iy)
787 int key = (ix + 0x7fff) | ((iy + 0x7fff) << 16);
788 GridPointCache::iterator it = _gridCache.find(key);
789 if (it != _gridCache.end()) {
793 SGGeod gp = SGGeod::fromDeg(
794 _gridCenter.getLongitudeDeg() + ix * _gridSpacing,
795 _gridCenter.getLatitudeDeg() + iy * _gridSpacing);
797 SGVec2d proj = project(gp);
798 _gridCache[key] = proj;
802 void MapWidget::drawLatLonGrid()
805 _gridCenter = roundGeod(_gridSpacing, _projectionCenter);
811 glColor4f(0.8, 0.8, 0.8, 0.4);
819 for (int x = -ix; x < ix; ++x) {
820 didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x+1, -iy));
821 didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x+1, iy));
822 didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x, -iy + 1));
823 didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x, iy - 1));
827 for (int y = -iy; y < iy; ++y) {
828 didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix, y+1));
829 didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix + 1, y));
830 didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix, y+1));
831 didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix - 1, y));
842 void MapWidget::drawGPSData()
844 std::string gpsMode = _gps->getStringValue("mode");
846 SGGeod wp0Geod = SGGeod::fromDeg(
847 _gps->getDoubleValue("wp/wp[0]/longitude-deg"),
848 _gps->getDoubleValue("wp/wp[0]/latitude-deg"));
850 SGGeod wp1Geod = SGGeod::fromDeg(
851 _gps->getDoubleValue("wp/wp[1]/longitude-deg"),
852 _gps->getDoubleValue("wp/wp[1]/latitude-deg"));
855 double gpsTrackDeg = _gps->getDoubleValue("indicated-track-true-deg");
856 double gpsSpeed = _gps->getDoubleValue("indicated-ground-speed-kt");
859 if (gpsSpeed > 3.0) { // only draw track line if valid
861 SGGeodesy::direct(_aircraft, gpsTrackDeg, _drawRangeNm * SG_NM_TO_METER, trackRadial, az2);
863 glColor4f(1.0, 1.0, 0.0, 1.0);
864 glEnable(GL_LINE_STIPPLE);
865 glLineStipple(1, 0x00FF);
866 drawLine(project(_aircraft), project(trackRadial));
867 glDisable(GL_LINE_STIPPLE);
870 if (gpsMode == "dto") {
871 SGVec2d wp0Pos = project(wp0Geod);
872 SGVec2d wp1Pos = project(wp1Geod);
874 glColor4f(1.0, 0.0, 1.0, 1.0);
875 drawLine(wp0Pos, wp1Pos);
879 if (_gps->getBoolValue("scratch/valid")) {
885 class MapAirportFilter : public FGAirport::AirportFilter
888 MapAirportFilter(SGPropertyNode_ptr nd)
890 _heliports = nd->getBoolValue("show-heliports", false);
891 _hardRunwaysOnly = nd->getBoolValue("hard-surfaced-airports", true);
892 _minLengthFt = nd->getDoubleValue("min-runway-length-ft", 2000.0);
895 virtual FGPositioned::Type maxType() const {
896 return _heliports ? FGPositioned::HELIPORT : FGPositioned::AIRPORT;
899 virtual bool passAirport(FGAirport* aApt) const {
900 if (_hardRunwaysOnly) {
901 return aApt->hasHardRunwayOfLengthFt(_minLengthFt);
909 bool _hardRunwaysOnly;
913 void MapWidget::drawAirports()
915 MapAirportFilter af(_root);
916 FGPositioned::List apts = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &af);
917 for (unsigned int i=0; i<apts.size(); ++i) {
918 drawAirport((FGAirport*) apts[i].get());
922 class NavaidFilter : public FGPositioned::Filter
925 NavaidFilter(bool fixesEnabled, bool navaidsEnabled) :
926 _fixes(fixesEnabled),
927 _navaids(navaidsEnabled)
930 virtual bool pass(FGPositioned* aPos) const {
931 if (_fixes && (aPos->type() == FGPositioned::FIX)) {
932 // ignore fixes which end in digits - expirmental
933 if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
941 virtual FGPositioned::Type minType() const {
942 return _fixes ? FGPositioned::FIX : FGPositioned::VOR;
945 virtual FGPositioned::Type maxType() const {
946 return _navaids ? FGPositioned::NDB : FGPositioned::FIX;
950 bool _fixes, _navaids;
953 void MapWidget::drawNavaids()
955 bool fixes = _root->getBoolValue("draw-fixes");
956 NavaidFilter f(fixes, _root->getBoolValue("draw-navaids"));
958 if (f.minType() <= f.maxType()) {
959 FGPositioned::List navs = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f);
962 for (unsigned int i=0; i<navs.size(); ++i) {
963 FGPositioned::Type ty = navs[i]->type();
964 if (ty == FGPositioned::NDB) {
965 drawNDB(false, (FGNavRecord*) navs[i].get());
966 } else if (ty == FGPositioned::VOR) {
967 drawVOR(false, (FGNavRecord*) navs[i].get());
968 } else if (ty == FGPositioned::FIX) {
969 drawFix((FGFix*) navs[i].get());
971 } // of navaid iteration
972 } // of navaids || fixes are drawn test
975 void MapWidget::drawNDB(bool tuned, FGNavRecord* ndb)
977 SGVec2d pos = project(ndb->geod());
980 glColor3f(0.0, 1.0, 1.0);
982 glColor3f(0.0, 0.0, 0.0);
985 glEnable(GL_LINE_STIPPLE);
986 glLineStipple(1, 0x00FF);
987 circleAt(pos, 20, 6);
988 circleAt(pos, 20, 10);
989 glDisable(GL_LINE_STIPPLE);
991 if (validDataForKey(ndb)) {
992 setAnchorForKey(ndb, pos);
997 ::snprintf(buffer, 1024, "%s\n%s %3.0fKhz",
998 ndb->name().c_str(), ndb->ident().c_str(),ndb->get_freq()/100.0);
1000 MapData* d = createDataForKey(ndb);
1002 d->setLabel(ndb->ident());
1004 d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1009 void MapWidget::drawVOR(bool tuned, FGNavRecord* vor)
1011 SGVec2d pos = project(vor->geod());
1013 glColor3f(0.0, 1.0, 1.0);
1015 glColor3f(0.0, 0.0, 1.0);
1018 circleAt(pos, 6, 8);
1020 if (validDataForKey(vor)) {
1021 setAnchorForKey(vor, pos);
1026 ::snprintf(buffer, 1024, "%s\n%s %6.3fMhz",
1027 vor->name().c_str(), vor->ident().c_str(),
1028 vor->get_freq() / 100.0);
1030 MapData* d = createDataForKey(vor);
1032 d->setLabel(vor->ident());
1033 d->setPriority(tuned ? 10000 : 100);
1034 d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1038 void MapWidget::drawFix(FGFix* fix)
1040 SGVec2d pos = project(fix->geod());
1041 glColor3f(0.0, 0.0, 0.0);
1042 circleAt(pos, 3, 6);
1044 if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1045 return; // hide fix labels beyond a certain zoom level
1048 if (validDataForKey(fix)) {
1049 setAnchorForKey(fix, pos);
1053 MapData* d = createDataForKey(fix);
1054 d->setLabel(fix->ident());
1056 d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1060 void MapWidget::drawNavRadio(SGPropertyNode_ptr radio)
1062 if (!radio || radio->getBoolValue("slaved-to-gps", false)
1063 || !radio->getBoolValue("in-range", false)) {
1067 if (radio->getBoolValue("nav-loc", false)) {
1068 drawTunedLocalizer(radio);
1071 // identify the tuned station - unfortunately we don't get lat/lon directly,
1072 // need to do the frequency search again
1073 double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1074 FGNavRecord* nav = globals->get_navlist()->findByFreq(mhz, _aircraft);
1075 if (!nav || (nav->ident() != radio->getStringValue("nav-id"))) {
1076 // mismatch between navradio selection logic and ours!
1083 SGVec2d pos = project(nav->geod());
1086 double trueRadial = radio->getDoubleValue("radials/target-radial-deg");
1087 SGGeodesy::direct(nav->geod(), trueRadial, nav->get_range() * SG_NM_TO_METER, range, az2);
1088 SGVec2d prange = project(range);
1090 SGVec2d norm = normalize(prange - pos);
1091 SGVec2d perp(norm.y(), -norm.x());
1093 circleAt(pos, 64, length(prange - pos));
1094 drawLine(pos, prange);
1096 // draw to/from arrows
1097 SGVec2d midPoint = (pos + prange) * 0.5;
1098 if (radio->getBoolValue("from-flag")) {
1104 SGVec2d arrowB = midPoint - (norm * sz) + (perp * sz);
1105 SGVec2d arrowC = midPoint - (norm * sz) - (perp * sz);
1106 drawLine(midPoint, arrowB);
1107 drawLine(arrowB, arrowC);
1108 drawLine(arrowC, midPoint);
1110 drawLine(pos, (2 * pos) - prange); // reciprocal radial
1113 void MapWidget::drawTunedLocalizer(SGPropertyNode_ptr radio)
1115 double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1116 FGNavRecord* loc = globals->get_loclist()->findByFreq(mhz, _aircraft);
1117 if (!loc || (loc->ident() != radio->getStringValue("nav-id"))) {
1118 // mismatch between navradio selection logic and ours!
1122 if (loc->runway()) {
1123 drawILS(true, loc->runway());
1128 void MapWidget::drawObstacle(FGPositioned* obs)
1130 SGVec2d pos = project(obs->geod());
1131 glColor3f(0.0, 0.0, 0.0);
1133 drawLine(pos, pos + SGVec2d());
1137 void MapWidget::drawAirport(FGAirport* apt)
1139 // draw tower location
1140 SGVec2d towerPos = project(apt->getTowerLocation());
1142 if (_cachedZoom <= SHOW_DETAIL_ZOOM) {
1143 glColor3f(1.0, 1.0, 1.0);
1146 drawLine(towerPos + SGVec2d(3, 0), towerPos + SGVec2d(3, 10));
1147 drawLine(towerPos + SGVec2d(-3, 0), towerPos + SGVec2d(-3, 10));
1148 drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(-3, 10));
1149 drawLine(towerPos + SGVec2d(6, 20), towerPos + SGVec2d(3, 10));
1150 drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(6, 20));
1153 if (validDataForKey(apt)) {
1154 setAnchorForKey(apt, towerPos);
1157 ::snprintf(buffer, 1024, "%s\n%s",
1158 apt->ident().c_str(), apt->name().c_str());
1160 MapData* d = createDataForKey(apt);
1162 d->setLabel(apt->ident());
1163 d->setPriority(100 + scoreAirportRunways(apt));
1164 d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 6);
1165 d->setAnchor(towerPos);
1168 if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1172 for (unsigned int r=0; r<apt->numRunways(); ++r) {
1173 FGRunway* rwy = apt->getRunwayByIndex(r);
1174 if (!rwy->isReciprocal()) {
1179 for (unsigned int r=0; r<apt->numRunways(); ++r) {
1180 FGRunway* rwy = apt->getRunwayByIndex(r);
1181 if (!rwy->isReciprocal()) {
1186 drawILS(false, rwy);
1188 } // of runway iteration
1192 int MapWidget::scoreAirportRunways(FGAirport* apt)
1194 bool needHardSurface = _root->getBoolValue("hard-surfaced-airports", true);
1195 double minLength = _root->getDoubleValue("min-runway-length-ft", 2000.0);
1198 unsigned int numRunways(apt->numRunways());
1199 for (unsigned int r=0; r<numRunways; ++r) {
1200 FGRunway* rwy = apt->getRunwayByIndex(r);
1201 if (rwy->isReciprocal()) {
1205 if (needHardSurface && !rwy->isHardSurface()) {
1209 if (rwy->lengthFt() < minLength) {
1213 int scoreLength = SGMiscd::roundToInt(rwy->lengthFt() / 200.0);
1214 score += scoreLength;
1215 } // of runways iteration
1220 void MapWidget::drawRunwayPre(FGRunway* rwy)
1222 SGVec2d p1 = project(rwy->begin());
1223 SGVec2d p2 = project(rwy->end());
1226 glColor3f(1.0, 0.0, 1.0);
1230 void MapWidget::drawRunway(FGRunway* rwy)
1233 // optionally show active, stopway, etc
1234 // in legend, show published heading and length
1235 // and threshold elevation
1237 SGVec2d p1 = project(rwy->begin());
1238 SGVec2d p2 = project(rwy->end());
1240 glColor3f(1.0, 1.0, 1.0);
1241 SGVec2d inset = normalize(p2 - p1) * 2;
1243 drawLine(p1 + inset, p2 - inset);
1245 if (validDataForKey(rwy)) {
1246 setAnchorForKey(rwy, (p1 + p2) * 0.5);
1251 ::snprintf(buffer, 1024, "%s/%s\n%03d/%03d\n%.0f'",
1252 rwy->ident().c_str(),
1253 rwy->reciprocalRunway()->ident().c_str(),
1254 displayHeading(rwy->headingDeg()),
1255 displayHeading(rwy->reciprocalRunway()->headingDeg()),
1258 MapData* d = createDataForKey(rwy);
1260 d->setLabel(rwy->ident() + "/" + rwy->reciprocalRunway()->ident());
1262 d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1263 d->setAnchor((p1 + p2) * 0.5);
1266 void MapWidget::drawILS(bool tuned, FGRunway* rwy)
1268 // arrow, tip centered on the landing threshold
1269 // using LOC transmitter position would be more accurate, but
1270 // is visually cluttered
1271 // arrow width is based upon the computed localizer width
1273 FGNavRecord* loc = rwy->ILS();
1274 double halfBeamWidth = loc->localizerWidth() * 0.5;
1275 SGVec2d t = project(rwy->threshold());
1277 double rangeM = loc->get_range() * SG_NM_TO_METER;
1278 double radial = loc->get_multiuse();
1279 SG_NORMALIZE_RANGE(radial, 0.0, 360.0);
1282 // compute the three end points at the widge end of the arrow
1283 SGGeodesy::direct(loc->geod(), radial, -rangeM, locEnd, az2);
1284 SGVec2d endCentre = project(locEnd);
1286 SGGeodesy::direct(loc->geod(), radial + halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1287 SGVec2d endR = project(locEnd);
1289 SGGeodesy::direct(loc->geod(), radial - halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1290 SGVec2d endL = project(locEnd);
1292 // outline two triangles
1295 glColor3f(0.0, 1.0, 1.0);
1297 glColor3f(0.0, 0.0, 1.0);
1300 glBegin(GL_LINE_LOOP);
1301 glVertex2dv(t.data());
1302 glVertex2dv(endCentre.data());
1303 glVertex2dv(endL.data());
1305 glBegin(GL_LINE_LOOP);
1306 glVertex2dv(t.data());
1307 glVertex2dv(endCentre.data());
1308 glVertex2dv(endR.data());
1311 if (validDataForKey(loc)) {
1312 setAnchorForKey(loc, endR);
1317 ::snprintf(buffer, 1024, "%s\n%s\n%03d - %3.2fMHz",
1318 loc->ident().c_str(), loc->name().c_str(),
1319 displayHeading(radial),
1320 loc->get_freq()/100.0);
1322 MapData* d = createDataForKey(loc);
1324 d->setLabel(loc->ident());
1326 d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1330 void MapWidget::drawTraffic()
1332 if (!_root->getBoolValue("draw-traffic")) {
1336 if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1340 const SGPropertyNode* ai = fgGetNode("/ai/models", true);
1342 for (int i = 0; i < ai->nChildren(); ++i) {
1343 const SGPropertyNode *model = ai->getChild(i);
1344 // skip bad or dead entries
1345 if (!model || model->getIntValue("id", -1) == -1) {
1349 const std::string& name(model->getName());
1350 SGGeod pos = SGGeod::fromDegFt(
1351 model->getDoubleValue("position/longitude-deg"),
1352 model->getDoubleValue("position/latitude-deg"),
1353 model->getDoubleValue("position/altitude-ft"));
1355 double dist = SGGeodesy::distanceNm(_projectionCenter, pos);
1356 if (dist > _drawRangeNm) {
1360 double heading = model->getDoubleValue("orientation/true-heading-deg");
1361 if ((name == "aircraft") || (name == "multiplayer") ||
1362 (name == "wingman") || (name == "tanker")) {
1363 drawAIAircraft(model, pos, heading);
1364 } else if ((name == "ship") || (name == "carrier") || (name == "escort")) {
1365 drawAIShip(model, pos, heading);
1367 } // of ai/models iteration
1370 void MapWidget::drawAIAircraft(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1373 SGVec2d p = project(pos);
1375 glColor3f(0.0, 0.0, 0.0);
1377 circleAt(p, 4, 6.0); // black diamond
1379 // draw heading vector
1380 int speedKts = static_cast<int>(model->getDoubleValue("velocities/true-airspeed-kt"));
1384 const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1385 double distanceM = speedKts * SG_NM_TO_METER * dt;
1389 SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1391 drawLine(p, project(advance));
1395 // draw callsign / altitude / speed
1397 ::snprintf(buffer, 1024, "%s\n%d'\n%dkts",
1398 model->getStringValue("callsign", "<>"),
1399 static_cast<int>(pos.getElevationFt() / 50.0) * 50,
1402 MapData* d = getOrCreateDataForKey((void*) model);
1404 d->setLabel(model->getStringValue("callsign", "<>"));
1405 d->setPriority(speedKts > 5 ? 60 : 10); // low priority for parked aircraft
1406 d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1411 void MapWidget::drawAIShip(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1413 SGVec2d p = project(pos);
1415 glColor3f(0.0, 0.0, 0.5);
1417 circleAt(p, 4, 6.0); // blue diamond (to differentiate from aircraft.
1419 // draw heading vector
1420 int speedKts = static_cast<int>(model->getDoubleValue("velocities/speed-kts"));
1424 const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1425 double distanceM = speedKts * SG_NM_TO_METER * dt;
1429 SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1431 drawLine(p, project(advance));
1434 // draw callsign / speed
1436 ::snprintf(buffer, 1024, "%s\n%dkts",
1437 model->getStringValue("name", "<>"),
1440 MapData* d = getOrCreateDataForKey((void*) model);
1442 d->setLabel(model->getStringValue("name", "<>"));
1443 d->setPriority(speedKts > 2 ? 30 : 10); // low priority for slow moving ships
1444 d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1448 SGVec2d MapWidget::project(const SGGeod& geod) const
1451 double r = earth_radius_lat(geod.getLatitudeRad());
1453 if (_orthoAzimuthProject) {
1454 // http://mathworld.wolfram.com/OrthographicProjection.html
1455 double cosTheta = cos(geod.getLatitudeRad());
1456 double sinDLambda = sin(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1457 double cosDLambda = cos(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1458 double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1459 double sinTheta = sin(geod.getLatitudeRad());
1460 double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1462 p = SGVec2d(cosTheta * sinDLambda,
1463 (cosTheta1 * sinTheta) - (sinTheta1 * cosTheta * cosDLambda)) * r * currentScale();
1466 // Sanson-Flamsteed projection, relative to the projection center
1467 double lonDiff = geod.getLongitudeRad() - _projectionCenter.getLongitudeRad(),
1468 latDiff = geod.getLatitudeRad() - _projectionCenter.getLatitudeRad();
1470 p = SGVec2d(cos(geod.getLatitudeRad()) * lonDiff, latDiff) * r * currentScale();
1474 // rotate as necessary
1475 double cost = cos(_upHeading * SG_DEGREES_TO_RADIANS),
1476 sint = sin(_upHeading * SG_DEGREES_TO_RADIANS);
1477 double rx = cost * p.x() - sint * p.y();
1478 double ry = sint * p.x() + cost * p.y();
1479 return SGVec2d(rx, ry);
1482 SGGeod MapWidget::unproject(const SGVec2d& p) const
1484 // unrotate, if necessary
1485 double cost = cos(-_upHeading * SG_DEGREES_TO_RADIANS),
1486 sint = sin(-_upHeading * SG_DEGREES_TO_RADIANS);
1487 SGVec2d ur(cost * p.x() - sint * p.y(),
1488 sint * p.x() + cost * p.y());
1490 double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1491 SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1493 if (_orthoAzimuthProject) {
1494 double phi = length(p);
1495 double c = asin(phi);
1496 double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1497 double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1499 double lat = asin(cos(c) * sinTheta1 + ((unscaled.y() * sin(c) * cosTheta1) / phi));
1500 double lon = _projectionCenter.getLongitudeRad() +
1501 atan((unscaled.x()* sin(c)) / (phi * cosTheta1 * cos(c) - unscaled.y() * sinTheta1 * sin(c)));
1502 return SGGeod::fromRad(lon, lat);
1504 double lat = unscaled.y() + _projectionCenter.getLatitudeRad();
1505 double lon = (unscaled.x() / cos(lat)) + _projectionCenter.getLongitudeRad();
1506 return SGGeod::fromRad(lon, lat);
1510 double MapWidget::currentScale() const
1512 return 1.0 / pow(2.0, _cachedZoom);
1515 void MapWidget::circleAt(const SGVec2d& center, int nSides, double r)
1517 glBegin(GL_LINE_LOOP);
1518 double advance = (SGD_PI * 2) / nSides;
1519 glVertex2d(center.x(), center.y() + r);
1521 for (int i=1; i<nSides; ++i) {
1522 glVertex2d(center.x() + (sin(t) * r), center.y() + (cos(t) * r));
1528 void MapWidget::circleAtAlt(const SGVec2d& center, int nSides, double r, double r2)
1530 glBegin(GL_LINE_LOOP);
1531 double advance = (SGD_PI * 2) / nSides;
1532 glVertex2d(center.x(), center.y() + r);
1534 for (int i=1; i<nSides; ++i) {
1535 double rr = (i%2 == 0) ? r : r2;
1536 glVertex2d(center.x() + (sin(t) * rr), center.y() + (cos(t) * rr));
1542 void MapWidget::drawLine(const SGVec2d& p1, const SGVec2d& p2)
1545 glVertex2dv(p1.data());
1546 glVertex2dv(p2.data());
1550 void MapWidget::drawLegendBox(const SGVec2d& pos, const std::string& t)
1552 std::vector<std::string> lines(simgear::strutils::split(t, "\n"));
1553 const int LINE_LEADING = 4;
1554 const int MARGIN = 4;
1557 int maxWidth = -1, totalHeight = 0;
1558 int lineHeight = legendFont.getStringHeight();
1560 for (unsigned int ln=0; ln<lines.size(); ++ln) {
1561 totalHeight += lineHeight;
1563 totalHeight += LINE_LEADING;
1566 int lw = legendFont.getStringWidth(lines[ln].c_str());
1567 maxWidth = std::max(maxWidth, lw);
1568 } // of line measurement
1571 return; // all lines are empty, don't draw
1574 totalHeight += MARGIN * 2;
1579 box.min[1] = -totalHeight;
1580 box.max[0] = maxWidth + (MARGIN * 2);
1583 box.draw (pos.x(), pos.y(), PUSTYLE_DROPSHADOW, colour, FALSE, border);
1586 int xPos = pos.x() + MARGIN;
1587 int yPos = pos.y() - (lineHeight + MARGIN);
1588 glColor3f(0.8, 0.8, 0.8);
1590 for (unsigned int ln=0; ln<lines.size(); ++ln) {
1591 legendFont.drawString(lines[ln].c_str(), xPos, yPos);
1592 yPos -= lineHeight + LINE_LEADING;
1596 void MapWidget::drawData()
1598 std::sort(_dataQueue.begin(), _dataQueue.end(), MapData::order);
1600 int hw = _width >> 1,
1602 puBox visBox(makePuBox(-hw, -hh, _width, _height));
1606 std::vector<MapData*> drawQueue;
1608 bool drawData = _root->getBoolValue("draw-data");
1609 const int MAX_DRAW_DATA = 25;
1610 const int MAX_DRAW = 50;
1612 for (; (d < _dataQueue.size()) && (drawn < MAX_DRAW); ++d) {
1613 MapData* md = _dataQueue[d];
1614 md->setDataVisible(drawData);
1616 if (md->isClipped(visBox)) {
1620 if (md->overlaps(drawQueue)) {
1621 if (drawData) { // overlapped with data, let's try just the label
1622 md->setDataVisible(false);
1623 if (md->overlaps(drawQueue)) {
1629 } // of overlaps case
1631 drawQueue.push_back(md);
1633 if (drawData && (drawn >= MAX_DRAW_DATA)) {
1638 // draw lowest-priority first, so higher-priorty items appear on top
1639 std::vector<MapData*>::reverse_iterator r;
1640 for (r = drawQueue.rbegin(); r!= drawQueue.rend(); ++r) {
1645 KeyDataMap::iterator it = _mapData.begin();
1646 for (; it != _mapData.end(); ) {
1648 if (it->second->isExpired()) {
1650 KeyDataMap::iterator cur = it++;
1651 _mapData.erase(cur);
1655 } // of expiry iteration
1658 bool MapWidget::validDataForKey(void* key)
1660 KeyDataMap::iterator it = _mapData.find(key);
1661 if (it == _mapData.end()) {
1662 return false; // no valid data for the key!
1665 it->second->resetAge(); // mark data as valid this frame
1666 _dataQueue.push_back(it->second);
1670 void MapWidget::setAnchorForKey(void* key, const SGVec2d& anchor)
1672 KeyDataMap::iterator it = _mapData.find(key);
1673 if (it == _mapData.end()) {
1674 throw sg_exception("no valid data for key!");
1677 it->second->setAnchor(anchor);
1680 MapData* MapWidget::getOrCreateDataForKey(void* key)
1682 KeyDataMap::iterator it = _mapData.find(key);
1683 if (it == _mapData.end()) {
1684 return createDataForKey(key);
1687 it->second->resetAge(); // mark data as valid this frame
1688 _dataQueue.push_back(it->second);
1692 MapData* MapWidget::createDataForKey(void* key)
1694 KeyDataMap::iterator it = _mapData.find(key);
1695 if (it != _mapData.end()) {
1696 throw sg_exception("duplicate data requested for key!");
1699 MapData* d = new MapData(0);
1701 _dataQueue.push_back(d);
1706 void MapWidget::clearData()
1708 KeyDataMap::iterator it = _mapData.begin();
1709 for (; it != _mapData.end(); ++it) {
1716 int MapWidget::displayHeading(double h) const
1718 if (_magneticHeadings) {
1719 h -= _magVar->get_magvar() * SG_RADIANS_TO_DEGREES;
1722 SG_NORMALIZE_RANGE(h, 0.0, 360.0);
1723 return SGMiscd::roundToInt(h);