]> git.mxchange.org Git - flightgear.git/blob - src/GUI/MapWidget.cxx
Reset: GUI can be shutdown.
[flightgear.git] / src / GUI / MapWidget.cxx
1 #ifdef HAVE_CONFIG_H
2 #  include "config.h"
3 #endif
4
5 #include "MapWidget.hxx"
6
7 #include <sstream>
8 #include <algorithm> // for std::sort
9 #include <plib/puAux.h>
10
11 #include <simgear/sg_inlines.h>
12 #include <simgear/misc/strutils.hxx>
13 #include <simgear/magvar/magvar.hxx>
14 #include <simgear/timing/sg_time.hxx> // for magVar julianDate
15 #include <simgear/structure/exception.hxx>
16
17 #include <Main/globals.hxx>
18 #include <Main/fg_props.hxx>
19 #include <Autopilot/route_mgr.hxx>
20 #include <Navaids/positioned.hxx>
21 #include <Navaids/navrecord.hxx>
22 #include <Navaids/navlist.hxx>
23 #include <Navaids/fix.hxx>
24 #include <Airports/airport.hxx>
25 #include <Airports/runways.hxx>
26 #include <Main/fg_os.hxx>      // fgGetKeyModifiers()
27 #include <Navaids/routePath.hxx>
28 #include <Aircraft/FlightHistory.hxx>
29 #include <AIModel/AIAircraft.hxx>
30 #include <AIModel/AIFlightPlan.hxx>
31
32 const char* RULER_LEGEND_KEY = "ruler-legend";
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 /************************************************************************
39   some trigonometric helper functions
40   (translated more or less directly from Alexei Novikovs perl original)
41 *************************************************************************/
42
43 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
44 static float earth_radius_lat( float lat )
45 {
46   double a = cos(lat)/rec;
47   double b = sin(lat)/rpol;
48   return 1.0f / sqrt( a * a + b * b );
49 }
50
51 ///////////////////////////////////////////////////////////////////////////
52
53 static puBox makePuBox(int x, int y, int w, int h)
54 {
55   puBox r;
56   r.min[0] = x;
57   r.min[1] = y;
58   r.max[0] =  x + w;
59   r.max[1] = y + h;
60   return r;
61 }
62
63 static bool puBoxIntersect(const puBox& a, const puBox& b)
64 {
65   int x0 = SG_MAX2(a.min[0], b.min[0]);
66   int y0 = SG_MAX2(a.min[1], b.min[1]);
67   int x1 = SG_MIN2(a.max[0], b.max[0]);
68   int y1 = SG_MIN2(a.max[1], b.max[1]);
69
70   return (x0 <= x1) && (y0 <= y1);
71 }
72
73 class MapData;
74 typedef std::vector<MapData*> MapDataVec;
75
76 class MapData
77 {
78 public:
79   static const int HALIGN_LEFT = 1;
80   static const int HALIGN_CENTER = 2;
81   static const int HALIGN_RIGHT = 3;
82
83   static const int VALIGN_TOP = 1 << 4;
84   static const int VALIGN_CENTER = 2 << 4;
85   static const int VALIGN_BOTTOM = 3 << 4;
86
87   MapData(int priority) :
88     _dirtyText(true),
89     _age(0),
90     _priority(priority),
91     _width(0),
92     _height(0),
93     _offsetDir(HALIGN_LEFT | VALIGN_CENTER),
94     _offsetPx(10),
95     _dataVisible(false)
96   {
97   }
98
99   void setLabel(const std::string& label)
100   {
101     if (label == _label) {
102       return; // common case, and saves invalidation
103     }
104
105     _label = label;
106     _dirtyText = true;
107   }
108
109   void setText(const std::string &text)
110   {
111     if (_rawText == text) {
112       return; // common case, and saves invalidation
113     }
114
115     _rawText = text;
116     _dirtyText = true;
117   }
118
119   void setDataVisible(bool vis) {
120     if (vis == _dataVisible) {
121       return;
122     }
123
124     if (_rawText.empty()) {
125       vis = false;
126     }
127
128     _dataVisible = vis;
129     _dirtyText = true;
130   }
131
132   static void setFont(puFont f)
133   {
134     _font = f;
135     _fontHeight = f.getStringHeight();
136     _fontDescender = f.getStringDescender();
137   }
138
139   static void setPalette(puColor* pal)
140   {
141     _palette = pal;
142   }
143
144   void setPriority(int pri)
145   {
146     _priority = pri;
147   }
148
149   int priority() const
150   { return _priority; }
151
152   void setAnchor(const SGVec2d& anchor)
153   {
154     _anchor = anchor;
155   }
156
157   void setOffset(int direction, int px)
158   {
159     if ((_offsetPx == px) && (_offsetDir == direction)) {
160       return;
161     }
162
163     _dirtyOffset = true;
164     _offsetDir = direction;
165     _offsetPx = px;
166   }
167
168   bool isClipped(const puBox& vis) const
169   {
170     validate();
171     if ((_width < 1) || (_height < 1)) {
172       return true;
173     }
174
175     return !puBoxIntersect(vis, box());
176   }
177
178   bool overlaps(const MapDataVec& l) const
179   {
180     validate();
181     puBox b(box());
182
183     MapDataVec::const_iterator it;
184     for (it = l.begin(); it != l.end(); ++it) {
185       if (puBoxIntersect(b, (*it)->box())) {
186         return true;
187       }
188     } // of list iteration
189
190     return false;
191   }
192
193   puBox box() const
194   {
195     validate();
196     return makePuBox(
197       _anchor.x() + _offset.x(),
198       _anchor.y() + _offset.y(),
199       _width, _height);
200   }
201
202   void draw()
203   {
204     validate();
205
206     int xx = _anchor.x() + _offset.x();
207     int yy = _anchor.y() + _offset.y();
208
209     if (_dataVisible) {
210       puBox box(makePuBox(0,0,_width, _height));
211       int border = 1;
212       box.draw(xx, yy, PUSTYLE_DROPSHADOW, _palette, FALSE, border);
213
214       // draw lines
215       int lineHeight = _fontHeight;
216       int xPos = xx + MARGIN;
217       int yPos = yy + _height - (lineHeight + MARGIN);
218       glColor3f(0.8, 0.8, 0.8);
219
220       for (unsigned int ln=0; ln<_lines.size(); ++ln) {
221         _font.drawString(_lines[ln].c_str(), xPos, yPos);
222         yPos -= lineHeight + LINE_LEADING;
223       }
224     } else {
225       glColor3f(0.8, 0.8, 0.8);
226       _font.drawString(_label.c_str(), xx, yy + _fontDescender);
227     }
228   }
229
230   void age()
231   {
232     ++_age;
233   }
234
235   void resetAge()
236   {
237     _age = 0;
238   }
239
240   bool isExpired() const
241   { return (_age > 100); }
242
243   static bool order(MapData* a, MapData* b)
244   {
245     return a->_priority > b->_priority;
246   }
247 private:
248   void validate() const
249   {
250     if (!_dirtyText) {
251       if (_dirtyOffset) {
252         computeOffset();
253       }
254
255       return;
256     }
257
258     if (_dataVisible) {
259       measureData();
260     } else {
261       measureLabel();
262     }
263
264     computeOffset();
265     _dirtyText = false;
266   }
267
268   void measureData() const
269   {
270     _lines = simgear::strutils::split(_rawText, "\n");
271   // measure text to find width and height
272     _width = -1;
273     _height = 0;
274
275     for (unsigned int ln=0; ln<_lines.size(); ++ln) {
276       _height += _fontHeight;
277       if (ln > 0) {
278         _height += LINE_LEADING;
279       }
280
281       int lw = _font.getStringWidth(_lines[ln].c_str());
282       _width = std::max(_width, lw);
283     } // of line measurement
284
285     if ((_width < 1) || (_height < 1)) {
286       // will be clipped
287       return;
288     }
289
290     _height += MARGIN * 2;
291     _width += MARGIN * 2;
292   }
293
294   void measureLabel() const
295   {
296     if (_label.empty()) {
297       _width = _height = -1;
298       return;
299     }
300
301     _height = _fontHeight;
302     _width = _font.getStringWidth(_label.c_str());
303   }
304
305   void computeOffset() const
306   {
307     _dirtyOffset = false;
308     if ((_width <= 0) || (_height <= 0)) {
309       return;
310     }
311
312     int hOffset = 0;
313     int vOffset = 0;
314
315     switch (_offsetDir & 0x0f) {
316     default:
317     case HALIGN_LEFT:
318       hOffset = _offsetPx;
319       break;
320
321     case HALIGN_CENTER:
322       hOffset = -(_width>>1);
323       break;
324
325     case HALIGN_RIGHT:
326       hOffset = -(_offsetPx + _width);
327       break;
328     }
329
330     switch (_offsetDir & 0xf0) {
331     default:
332     case VALIGN_TOP:
333       vOffset = -(_offsetPx + _height);
334       break;
335
336     case VALIGN_CENTER:
337       vOffset = -(_height>>1);
338       break;
339
340     case VALIGN_BOTTOM:
341       vOffset = _offsetPx;
342       break;
343     }
344
345     _offset = SGVec2d(hOffset, vOffset);
346   }
347
348   static const int LINE_LEADING = 3;
349         static const int MARGIN = 3;
350
351   mutable bool _dirtyText;
352   mutable bool _dirtyOffset;
353   int _age;
354   std::string _rawText;
355   std::string _label;
356   mutable std::vector<std::string> _lines;
357   int _priority;
358   mutable int _width, _height;
359   SGVec2d _anchor;
360   int _offsetDir;
361   int _offsetPx;
362   mutable SGVec2d _offset;
363   bool _dataVisible;
364
365   static puFont _font;
366   static puColor* _palette;
367   static int _fontHeight;
368   static int _fontDescender;
369 };
370
371 puFont MapData::_font;
372 puColor* MapData::_palette;
373 int MapData::_fontHeight = 0;
374 int MapData::_fontDescender = 0;
375
376 ///////////////////////////////////////////////////////////////////////////
377
378 const int MAX_ZOOM = 12;
379 const int SHOW_DETAIL_ZOOM = 8;
380 const int SHOW_DETAIL2_ZOOM = 5;
381 const int CURSOR_PAN_STEP = 32;
382
383 MapWidget::MapWidget(int x, int y, int maxX, int maxY) :
384   puObject(x,y,maxX, maxY)
385 {
386   _route = static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"));
387   _gps = fgGetNode("/instrumentation/gps");
388
389   _width = maxX - x;
390   _height = maxY - y;
391   _hasPanned = false;
392   _projection = PROJECTION_SAMSON_FLAMSTEED;
393   _magneticHeadings = false;
394   
395   MapData::setFont(legendFont);
396   MapData::setPalette(colour);
397
398   _magVar = new SGMagVar();
399 }
400
401 MapWidget::~MapWidget()
402 {
403   delete _magVar;
404   clearData();
405 }
406
407 void MapWidget::setProperty(SGPropertyNode_ptr prop)
408 {
409   _root = prop;
410   int zoom = _root->getIntValue("zoom", -1);
411   if (zoom < 0) {
412     _root->setIntValue("zoom", 6); // default zoom
413   }
414   
415 // expose MAX_ZOOM to the UI
416   _root->setIntValue("max-zoom", MAX_ZOOM);
417   _root->setBoolValue("centre-on-aircraft", true);
418   _root->setBoolValue("draw-data", false);
419   _root->setBoolValue("draw-flight-history", false);
420   _root->setBoolValue("magnetic-headings", true);
421 }
422
423 void MapWidget::setSize(int w, int h)
424 {
425   puObject::setSize(w, h);
426
427   _width = w;
428   _height = h;
429
430 }
431
432 void MapWidget::doHit( int button, int updown, int x, int y )
433 {
434   puObject::doHit(button, updown, x, y);
435   if (updown == PU_DRAG) {
436     handlePan(x, y);
437     return;
438   }
439
440   if (button == 3) { // mouse-wheel up
441     zoomIn();
442   } else if (button == 4) { // mouse-wheel down
443     zoomOut();
444   }
445
446   if (button != active_mouse_button) {
447     return;
448   }
449
450   _hitLocation = SGVec2d(x - abox.min[0], y - abox.min[1]);
451
452   if (updown == PU_UP) {
453     puDeactivateWidget();
454   } else if (updown == PU_DOWN) {
455     puSetActiveWidget(this, x, y);
456
457     if (fgGetKeyModifiers() & KEYMOD_CTRL) {
458       _clickGeod = unproject(_hitLocation - SGVec2d(_width>>1, _height>>1));
459     }
460   }
461 }
462
463 void MapWidget::handlePan(int x, int y)
464 {
465   SGVec2d delta = SGVec2d(x, y) - _hitLocation;
466   pan(delta);
467   _hitLocation = SGVec2d(x,y);
468 }
469
470 int MapWidget::checkKey (int key, int updown )
471 {
472   if ((updown == PU_UP) || !isVisible () || !isActive () || (window != puGetWindow())) {
473     return FALSE ;
474   }
475
476   switch (key)
477   {
478
479   case PU_KEY_UP:
480     pan(SGVec2d(0, -CURSOR_PAN_STEP));
481     break;
482
483   case PU_KEY_DOWN:
484     pan(SGVec2d(0, CURSOR_PAN_STEP));
485     break ;
486
487   case PU_KEY_LEFT:
488     pan(SGVec2d(CURSOR_PAN_STEP, 0));
489     break;
490
491   case PU_KEY_RIGHT:
492     pan(SGVec2d(-CURSOR_PAN_STEP, 0));
493     break;
494
495   case '-':
496     zoomOut();
497
498     break;
499
500   case '=':
501     zoomIn();
502     break;
503
504   default :
505     return FALSE;
506   }
507
508   return TRUE ;
509 }
510
511 void MapWidget::pan(const SGVec2d& delta)
512 {
513   _hasPanned = true; 
514   _projectionCenter = unproject(-delta);
515 }
516
517 int MapWidget::zoom() const
518 {
519   int z = _root->getIntValue("zoom");
520   SG_CLAMP_RANGE(z, 0, MAX_ZOOM);
521   return z;
522 }
523
524 void MapWidget::zoomIn()
525 {
526   if (zoom() >= MAX_ZOOM) {
527     return;
528   }
529
530   _root->setIntValue("zoom", zoom() + 1);
531 }
532
533 void MapWidget::zoomOut()
534 {
535   if (zoom() <= 0) {
536     return;
537   }
538
539   _root->setIntValue("zoom", zoom() - 1);
540 }
541
542 void MapWidget::draw(int dx, int dy)
543 {
544   _aircraft = globals->get_aircraft_position();
545     
546   bool mag = _root->getBoolValue("magnetic-headings");
547   if (mag != _magneticHeadings) {
548     clearData(); // flush cached data text, since it often includes heading
549     _magneticHeadings =  mag;
550   }
551   
552   if (_hasPanned) {
553       _root->setBoolValue("centre-on-aircraft", false);
554       _hasPanned = false;
555   }
556   else if (_root->getBoolValue("centre-on-aircraft")) {
557     _projectionCenter = _aircraft;
558   }
559
560   double julianDate = globals->get_time_params()->getJD();
561   _magVar->update(_projectionCenter, julianDate);
562
563   bool aircraftUp = _root->getBoolValue("aircraft-heading-up");
564   if (aircraftUp) {
565     _upHeading = fgGetDouble("/orientation/heading-deg");
566   } else {
567     _upHeading = 0.0;
568   }
569
570   _cachedZoom = MAX_ZOOM - zoom();
571   SGGeod topLeft = unproject(SGVec2d(_width/2, _height/2));
572   // compute draw range, including a fudge factor for ILSs and other 'long'
573   // symbols
574   _drawRangeNm = SGGeodesy::distanceNm(_projectionCenter, topLeft) + 10.0;
575
576 // drawing operations
577   GLint sx = (int) abox.min[0],
578     sy = (int) abox.min[1];
579   glScissor(dx + sx, dy + sy, _width, _height);
580   glEnable(GL_SCISSOR_TEST);
581
582   glMatrixMode(GL_MODELVIEW);
583   glPushMatrix();
584   // cetere drawing about the widget center (which is also the
585   // projection centre)
586   glTranslated(dx + sx + (_width/2), dy + sy + (_height/2), 0.0);
587
588   drawLatLonGrid();
589
590   if (aircraftUp) {
591     int textHeight = legendFont.getStringHeight() + 5;
592
593     // draw heading line
594     SGVec2d loc = project(_aircraft);
595     glColor3f(1.0, 1.0, 1.0);
596     drawLine(loc, SGVec2d(loc.x(), (_height / 2) - textHeight));
597
598     int displayHdg;
599     if (_magneticHeadings) {
600       displayHdg = (int) fgGetDouble("/orientation/heading-magnetic-deg");
601     } else {
602       displayHdg = (int) _upHeading;
603     }
604
605     double y = (_height / 2) - textHeight;
606     char buf[16];
607     ::snprintf(buf, 16, "%d", displayHdg);
608     int sw = legendFont.getStringWidth(buf);
609     legendFont.drawString(buf, loc.x() - sw/2, y);
610   }
611
612   drawAirports();
613   drawNavaids();
614   drawPOIs();
615   drawTraffic();
616   drawGPSData();
617   drawNavRadio(fgGetNode("/instrumentation/nav[0]", false));
618   drawNavRadio(fgGetNode("/instrumentation/nav[1]", false));
619   paintAircraftLocation(_aircraft);
620   drawFlightHistory();
621   paintRoute();
622   paintRuler();
623
624   drawData();
625
626   glPopMatrix();
627   glDisable(GL_SCISSOR_TEST);
628 }
629
630 void MapWidget::paintRuler()
631 {
632   if (_clickGeod == SGGeod()) {
633     return;
634   }
635
636   SGVec2d acftPos = project(_aircraft);
637   SGVec2d clickPos = project(_clickGeod);
638
639   glColor4f(0.0, 1.0, 1.0, 0.6);
640   drawLine(acftPos, clickPos);
641
642   circleAtAlt(clickPos, 8, 10, 5);
643
644   double dist, az, az2;
645   SGGeodesy::inverse(_aircraft, _clickGeod, az, az2, dist);
646   char buffer[1024];
647         ::snprintf(buffer, 1024, "%03d/%.1fnm",
648                 displayHeading(az), dist * SG_METER_TO_NM);
649
650   MapData* d = getOrCreateDataForKey((void*) RULER_LEGEND_KEY);
651   d->setLabel(buffer);
652   d->setAnchor(clickPos);
653   d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
654   d->setPriority(20000);
655
656
657 }
658
659 void MapWidget::paintAircraftLocation(const SGGeod& aircraftPos)
660 {
661   SGVec2d loc = project(aircraftPos);
662
663   double hdg = fgGetDouble("/orientation/heading-deg");
664
665   glLineWidth(2.0);
666   glColor4f(1.0, 1.0, 0.0, 1.0);
667   glPushMatrix();
668   glTranslated(loc.x(), loc.y(), 0.0);
669   glRotatef(hdg - _upHeading, 0.0, 0.0, -1.0);
670
671   const SGVec2d wingspan(12, 0);
672   const SGVec2d nose(0, 8);
673   const SGVec2d tail(0, -14);
674   const SGVec2d tailspan(4,0);
675
676   drawLine(-wingspan, wingspan);
677   drawLine(nose, tail);
678   drawLine(tail - tailspan, tail + tailspan);
679
680   glPopMatrix();
681   glLineWidth(1.0);
682 }
683
684 void MapWidget::paintRoute()
685 {
686   if (_route->numWaypts() < 2) {
687     return;
688   }
689
690   RoutePath path(_route->flightPlan());
691
692 // first pass, draw the actual lines
693   glLineWidth(2.0);
694
695   for (int w=0; w<_route->numWaypts(); ++w) {
696     SGGeodVec gv(path.pathForIndex(w));
697     if (gv.empty()) {
698       continue;
699     }
700
701     if (w < _route->currentIndex()) {
702       glColor4f(0.5, 0.5, 0.5, 0.7);
703     } else {
704       glColor4f(1.0, 0.0, 1.0, 1.0);
705     }
706
707     flightgear::WayptRef wpt(_route->wayptAtIndex(w));
708     if (wpt->flag(flightgear::WPT_MISS)) {
709       glEnable(GL_LINE_STIPPLE);
710       glLineStipple(1, 0x00FF);
711     }
712
713     glBegin(GL_LINE_STRIP);
714     for (unsigned int i=0; i<gv.size(); ++i) {
715       SGVec2d p = project(gv[i]);
716       glVertex2d(p.x(), p.y());
717     }
718
719     glEnd();
720     glDisable(GL_LINE_STIPPLE);
721   }
722
723   glLineWidth(1.0);
724 // second pass, draw waypoint symbols and data
725   for (int w=0; w < _route->numWaypts(); ++w) {
726     flightgear::WayptRef wpt(_route->wayptAtIndex(w));
727     SGGeod g = path.positionForIndex(w);
728     if (g == SGGeod()) {
729       continue; // Vectors or similar
730     }
731
732     SGVec2d p = project(g);
733     glColor4f(1.0, 0.0, 1.0, 1.0);
734     circleAtAlt(p, 8, 12, 5);
735
736     std::ostringstream legend;
737     legend << wpt->ident();
738     if (wpt->altitudeRestriction() != flightgear::RESTRICT_NONE) {
739       legend << '\n' << SGMiscd::roundToInt(wpt->altitudeFt()) << '\'';
740     }
741
742     if (wpt->speedRestriction() == flightgear::SPEED_RESTRICT_MACH) {
743       legend << '\n' << wpt->speedMach() << "M";
744     } else if (wpt->speedRestriction() != flightgear::RESTRICT_NONE) {
745       legend << '\n' << SGMiscd::roundToInt(wpt->speedKts()) << "Kts";
746     }
747
748     MapData* d = getOrCreateDataForKey(reinterpret_cast<void*>(w * 2));
749     d->setText(legend.str());
750     d->setLabel(wpt->ident());
751     d->setAnchor(p);
752     d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
753     d->setPriority(w < _route->currentIndex() ? 9000 : 12000);
754
755   } // of second waypoint iteration
756 }
757
758 void MapWidget::drawFlightHistory()
759 {
760   FGFlightHistory* history = (FGFlightHistory*) globals->get_subsystem("history");
761   if (!history || !_root->getBoolValue("draw-flight-history")) {
762     return;
763   }
764   
765   // first pass, draw the actual lines
766   glLineWidth(2.0);
767   
768   SGGeodVec gv(history->pathForHistory());
769   glColor4f(0.0, 0.0, 1.0, 0.7);
770
771   glBegin(GL_LINE_STRIP);
772   for (unsigned int i=0; i<gv.size(); ++i) {
773     SGVec2d p = project(gv[i]);
774     glVertex2d(p.x(), p.y());
775   }
776   
777   glEnd();
778 }
779
780 /**
781  * Round a SGGeod to an arbitrary precision.
782  * For example, passing precision of 0.5 will round to the nearest 0.5 of
783  * a degree in both lat and lon - passing in 3.0 rounds to the nearest 3 degree
784  * multiple, and so on.
785  */
786 static SGGeod roundGeod(double precision, const SGGeod& g)
787 {
788   double lon = SGMiscd::round(g.getLongitudeDeg() / precision);
789   double lat = SGMiscd::round(g.getLatitudeDeg() / precision);
790
791   return SGGeod::fromDeg(lon * precision, lat * precision);
792 }
793
794 bool MapWidget::drawLineClipped(const SGVec2d& a, const SGVec2d& b)
795 {
796   double minX = SGMiscd::min(a.x(), b.x()),
797     minY = SGMiscd::min(a.y(), b.y()),
798     maxX = SGMiscd::max(a.x(), b.x()),
799     maxY = SGMiscd::max(a.y(), b.y());
800
801   int hh = _height >> 1, hw = _width >> 1;
802
803   if ((maxX < -hw) || (minX > hw) || (minY > hh) || (maxY < -hh)) {
804     return false;
805   }
806
807   glVertex2dv(a.data());
808   glVertex2dv(b.data());
809   return true;
810 }
811
812 SGVec2d MapWidget::gridPoint(int ix, int iy)
813 {
814         int key = (ix + 0x7fff) | ((iy + 0x7fff) << 16);
815         GridPointCache::iterator it = _gridCache.find(key);
816         if (it != _gridCache.end()) {
817                 return it->second;
818         }
819
820         SGGeod gp = SGGeod::fromDeg(
821     _gridCenter.getLongitudeDeg() + ix * _gridSpacing,
822                 _gridCenter.getLatitudeDeg() + iy * _gridSpacing);
823
824         SGVec2d proj = project(gp);
825         _gridCache[key] = proj;
826         return proj;
827 }
828
829 void MapWidget::drawLatLonGrid()
830 {
831   _gridSpacing = 1.0;
832   _gridCenter = roundGeod(_gridSpacing, _projectionCenter);
833   _gridCache.clear();
834
835   int ix = 0;
836   int iy = 0;
837
838   glColor4f(0.8, 0.8, 0.8, 0.4);
839   glBegin(GL_LINES);
840   bool didDraw;
841   do {
842     didDraw = false;
843     ++ix;
844     ++iy;
845
846     for (int x = -ix; x < ix; ++x) {
847       didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x+1, -iy));
848       didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x+1, iy));
849       didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x, -iy + 1));
850       didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x, iy - 1));
851
852     }
853
854     for (int y = -iy; y < iy; ++y) {
855       didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix, y+1));
856       didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix + 1, y));
857       didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix, y+1));
858       didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix - 1, y));
859     }
860
861     if (ix > 30) {
862       break;
863     }
864   } while (didDraw);
865
866   glEnd();
867 }
868
869 void MapWidget::drawGPSData()
870 {
871   std::string gpsMode = _gps->getStringValue("mode");
872
873   SGGeod wp0Geod = SGGeod::fromDeg(
874         _gps->getDoubleValue("wp/wp[0]/longitude-deg"),
875         _gps->getDoubleValue("wp/wp[0]/latitude-deg"));
876
877   SGGeod wp1Geod = SGGeod::fromDeg(
878         _gps->getDoubleValue("wp/wp[1]/longitude-deg"),
879         _gps->getDoubleValue("wp/wp[1]/latitude-deg"));
880
881 // draw track line
882   double gpsTrackDeg = _gps->getDoubleValue("indicated-track-true-deg");
883   double gpsSpeed = _gps->getDoubleValue("indicated-ground-speed-kt");
884   double az2;
885
886   if (gpsSpeed > 3.0) { // only draw track line if valid
887     SGGeod trackRadial;
888     SGGeodesy::direct(_aircraft, gpsTrackDeg, _drawRangeNm * SG_NM_TO_METER, trackRadial, az2);
889
890     glColor4f(1.0, 1.0, 0.0, 1.0);
891     glEnable(GL_LINE_STIPPLE);
892     glLineStipple(1, 0x00FF);
893     drawLine(project(_aircraft), project(trackRadial));
894     glDisable(GL_LINE_STIPPLE);
895   }
896
897   if (gpsMode == "dto") {
898     SGVec2d wp0Pos = project(wp0Geod);
899     SGVec2d wp1Pos = project(wp1Geod);
900
901     glColor4f(1.0, 0.0, 1.0, 1.0);
902     drawLine(wp0Pos, wp1Pos);
903
904   }
905
906   if (_gps->getBoolValue("scratch/valid")) {
907     // draw scratch data
908
909   }
910 }
911
912 class MapAirportFilter : public FGAirport::AirportFilter
913 {
914 public:
915   MapAirportFilter(SGPropertyNode_ptr nd)
916   {
917     _heliports = nd->getBoolValue("show-heliports", false);
918     _hardRunwaysOnly = nd->getBoolValue("hard-surfaced-airports", true);
919     _minLengthFt = fgGetDouble("/sim/navdb/min-runway-length-ft", 2000);
920   }
921
922   virtual FGPositioned::Type maxType() const {
923     return _heliports ? FGPositioned::HELIPORT : FGPositioned::AIRPORT;
924   }
925
926   virtual bool passAirport(FGAirport* aApt) const {
927     if (_hardRunwaysOnly) {
928       return aApt->hasHardRunwayOfLengthFt(_minLengthFt);
929     }
930
931     return true;
932   }
933     
934     void showAll()
935     {
936         _hardRunwaysOnly = false;
937     }
938
939 private:
940   bool _heliports;
941   bool _hardRunwaysOnly;
942   double _minLengthFt;
943 };
944
945 void MapWidget::drawAirports()
946 {
947   MapAirportFilter af(_root);
948     if (_cachedZoom <= SHOW_DETAIL2_ZOOM) {
949         // show all airports when zoomed in sufficently
950         af.showAll();
951     }
952     
953   bool partial = false;
954   FGPositionedList apts = FGPositioned::findWithinRangePartial(_projectionCenter, _drawRangeNm, &af, partial);
955   for (unsigned int i=0; i<apts.size(); ++i) {
956     drawAirport((FGAirport*) apts[i].get());
957   }
958 }
959
960 class NavaidFilter : public FGPositioned::Filter
961 {
962 public:
963   NavaidFilter(bool fixesEnabled, bool navaidsEnabled) :
964     _fixes(fixesEnabled),
965     _navaids(navaidsEnabled)
966   {}
967
968   virtual bool pass(FGPositioned* aPos) const {
969     if (_fixes && (aPos->type() == FGPositioned::FIX)) {
970       // ignore fixes which end in digits - expirmental
971       if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
972         return false;
973       }
974     }
975
976     return true;
977   }
978
979   virtual FGPositioned::Type minType() const {
980     return _fixes ? FGPositioned::FIX : FGPositioned::NDB;
981   }
982
983   virtual FGPositioned::Type maxType() const {
984     return _navaids ? FGPositioned::VOR : FGPositioned::FIX;
985   }
986
987 private:
988   bool _fixes, _navaids;
989 };
990
991 void MapWidget::drawNavaids()
992 {
993   bool fixes = _root->getBoolValue("draw-fixes");
994   NavaidFilter f(fixes, _root->getBoolValue("draw-navaids"));
995
996   if (f.minType() <= f.maxType()) {
997     FGPositionedList navs = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f);
998
999     glLineWidth(1.0);
1000     for (unsigned int i=0; i<navs.size(); ++i) {
1001       FGPositioned::Type ty = navs[i]->type();
1002       if (ty == FGPositioned::NDB) {
1003         drawNDB(false, (FGNavRecord*) navs[i].get());
1004       } else if (ty == FGPositioned::VOR) {
1005         drawVOR(false, (FGNavRecord*) navs[i].get());
1006       } else if (ty == FGPositioned::FIX) {
1007         drawFix((FGFix*) navs[i].get());
1008       }
1009     } // of navaid iteration
1010   } // of navaids || fixes are drawn test
1011 }
1012
1013 void MapWidget::drawPOIs()
1014 {
1015   FGPositioned::TypeFilter f(FGPositioned::COUNTRY);
1016   f.addType(FGPositioned::CITY);
1017   f.addType(FGPositioned::TOWN);
1018   FGPositionedList poi = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f);
1019
1020     glLineWidth(1.0);
1021     for (unsigned int i=0; i<poi.size(); ++i) {
1022       FGPositioned::Type ty = poi[i]->type();
1023       if (ty == FGPositioned::COUNTRY) {
1024         drawCountries((FGNavRecord*) poi[i].get());
1025       } else if (ty == FGPositioned::CITY) {
1026         drawCities((FGNavRecord*) poi[i].get());
1027       } else if (ty == FGPositioned::TOWN) {
1028         drawTowns((FGNavRecord*) poi[i].get());
1029       }
1030     } // of navaid iteration
1031 }
1032
1033 void MapWidget::drawNDB(bool tuned, FGNavRecord* ndb)
1034 {
1035   SGVec2d pos = project(ndb->geod());
1036
1037   if (tuned) {
1038     glColor3f(0.0, 1.0, 1.0);
1039   } else {
1040     glColor3f(0.0, 0.0, 0.0);
1041   }
1042
1043   glEnable(GL_LINE_STIPPLE);
1044   glLineStipple(1, 0x00FF);
1045   circleAt(pos, 20, 6);
1046   circleAt(pos, 20, 10);
1047   glDisable(GL_LINE_STIPPLE);
1048
1049   if (validDataForKey(ndb)) {
1050     setAnchorForKey(ndb, pos);
1051     return;
1052   }
1053
1054   char buffer[1024];
1055         ::snprintf(buffer, 1024, "%s\n%s %3.0fKhz",
1056                 ndb->name().c_str(), ndb->ident().c_str(),ndb->get_freq()/100.0);
1057
1058   MapData* d = createDataForKey(ndb);
1059   d->setPriority(40);
1060   d->setLabel(ndb->ident());
1061   d->setText(buffer);
1062   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1063   d->setAnchor(pos);
1064
1065 }
1066
1067 void MapWidget::drawVOR(bool tuned, FGNavRecord* vor)
1068 {
1069   SGVec2d pos = project(vor->geod());
1070   if (tuned) {
1071     glColor3f(0.0, 1.0, 1.0);
1072   } else {
1073     glColor3f(0.0, 0.0, 1.0);
1074   }
1075
1076   circleAt(pos, 6, 9);
1077   circleAt(pos, 8, 1);
1078
1079   if (vor->hasDME())
1080   squareAt(pos, 9);
1081
1082   if (validDataForKey(vor)) {
1083     setAnchorForKey(vor, pos);
1084     return;
1085   }
1086
1087   char buffer[1024];
1088         ::snprintf(buffer, 1024, "%s\n%s %6.3fMhz",
1089                 vor->name().c_str(), vor->ident().c_str(),
1090     vor->get_freq() / 100.0);
1091
1092   MapData* d = createDataForKey(vor);
1093   d->setText(buffer);
1094   d->setLabel(vor->ident());
1095   d->setPriority(tuned ? 10000 : 100);
1096   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1097   d->setAnchor(pos);
1098 }
1099
1100 void MapWidget::drawFix(FGFix* fix)
1101 {
1102   SGVec2d pos = project(fix->geod());
1103   glColor3f(0.0, 0.0, 0.0);
1104   circleAt(pos, 3, 6);
1105
1106   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1107     return; // hide fix labels beyond a certain zoom level
1108   }
1109
1110   if (validDataForKey(fix)) {
1111     setAnchorForKey(fix, pos);
1112     return;
1113   }
1114
1115   MapData* d = createDataForKey(fix);
1116   d->setLabel(fix->ident());
1117   d->setPriority(20);
1118   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1119   d->setAnchor(pos);
1120 }
1121
1122 void MapWidget::drawNavRadio(SGPropertyNode_ptr radio)
1123 {
1124   if (!radio || radio->getBoolValue("slaved-to-gps", false)
1125         || !radio->getBoolValue("in-range", false)) {
1126     return;
1127   }
1128
1129   if (radio->getBoolValue("nav-loc", false)) {
1130     drawTunedLocalizer(radio);
1131   }
1132
1133   // identify the tuned station - unfortunately we don't get lat/lon directly,
1134   // need to do the frequency search again
1135   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1136
1137   FGNavRecord* nav = FGNavList::findByFreq(mhz, _aircraft,
1138                                            FGNavList::navFilter());
1139   if (!nav || (nav->ident() != radio->getStringValue("nav-id"))) {
1140     // mismatch between navradio selection logic and ours!
1141     return;
1142   }
1143
1144   glLineWidth(1.0);
1145   drawVOR(true, nav);
1146
1147   SGVec2d pos = project(nav->geod());
1148   SGGeod range;
1149   double az2;
1150   double trueRadial = radio->getDoubleValue("radials/target-radial-deg");
1151   SGGeodesy::direct(nav->geod(), trueRadial, nav->get_range() * SG_NM_TO_METER, range, az2);
1152   SGVec2d prange = project(range);
1153
1154   SGVec2d norm = normalize(prange - pos);
1155   SGVec2d perp(norm.y(), -norm.x());
1156
1157   circleAt(pos, 64, length(prange - pos));
1158   drawLine(pos, prange);
1159
1160 // draw to/from arrows
1161   SGVec2d midPoint = (pos + prange) * 0.5;
1162   if (radio->getBoolValue("from-flag")) {
1163     norm = -norm;
1164     perp = -perp;
1165   }
1166
1167   int sz = 10;
1168   SGVec2d arrowB = midPoint - (norm * sz) + (perp * sz);
1169   SGVec2d arrowC = midPoint - (norm * sz) - (perp * sz);
1170   drawLine(midPoint, arrowB);
1171   drawLine(arrowB, arrowC);
1172   drawLine(arrowC, midPoint);
1173
1174   drawLine(pos, (2 * pos) - prange); // reciprocal radial
1175 }
1176
1177 void MapWidget::drawTunedLocalizer(SGPropertyNode_ptr radio)
1178 {
1179   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1180   FGNavRecord* loc = FGNavList::findByFreq(mhz, _aircraft, FGNavList::locFilter());
1181   if (!loc || (loc->ident() != radio->getStringValue("nav-id"))) {
1182     // mismatch between navradio selection logic and ours!
1183     return;
1184   }
1185
1186   if (loc->runway()) {
1187     drawILS(true, loc->runway());
1188   }
1189 }
1190
1191 void MapWidget::drawCountries(FGNavRecord* rec)
1192 {
1193   if (_cachedZoom < 9) {
1194     return; // hide labels beyond a certain zoom level
1195   }
1196
1197   SGVec2d pos = project(rec->geod());
1198   glColor3f(1.0, 1.0, 0.0);
1199
1200   circleAt(pos, 4, 10);
1201
1202   if (validDataForKey(rec)) {
1203     setAnchorForKey(rec, pos);
1204     return;
1205   }
1206
1207   char buffer[1024];
1208         ::snprintf(buffer, 1024, "%s",
1209                 rec->name().c_str());
1210
1211   MapData* d = createDataForKey(rec);
1212   d->setPriority(200);
1213   d->setLabel(rec->ident());
1214   d->setText(buffer);
1215   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1216   d->setAnchor(pos);
1217
1218 }
1219
1220 void MapWidget::drawCities(FGNavRecord* rec)
1221 {
1222   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1223     return; // hide labels beyond a certain zoom level
1224   }
1225
1226   SGVec2d pos = project(rec->geod());
1227   glColor3f(0.0, 1.0, 0.0);
1228
1229   circleAt(pos, 4, 8);
1230
1231   if (validDataForKey(rec)) {
1232     setAnchorForKey(rec, pos);
1233     return;
1234   }
1235
1236   char buffer[1024];
1237         ::snprintf(buffer, 1024, "%s",
1238                 rec->name().c_str());
1239
1240   MapData* d = createDataForKey(rec);
1241   d->setPriority(40);
1242   d->setLabel(rec->ident());
1243   d->setText(buffer);
1244   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1245   d->setAnchor(pos);
1246
1247 }
1248
1249 void MapWidget::drawTowns(FGNavRecord* rec)
1250 {
1251   if (_cachedZoom > SHOW_DETAIL2_ZOOM) {
1252     return; // hide labels beyond a certain zoom level
1253   }
1254
1255   SGVec2d pos = project(rec->geod());
1256   glColor3f(0.2, 1.0, 0.0);
1257
1258   circleAt(pos, 4, 5);
1259
1260   if (validDataForKey(rec)) {
1261     setAnchorForKey(rec, pos);
1262     return;
1263   }
1264
1265   char buffer[1024];
1266         ::snprintf(buffer, 1024, "%s",
1267                 rec->name().c_str());
1268
1269   MapData* d = createDataForKey(rec);
1270   d->setPriority(40);
1271   d->setLabel(rec->ident());
1272   d->setText(buffer);
1273   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1274   d->setAnchor(pos);
1275
1276 }
1277
1278 /*
1279 void MapWidget::drawObstacle(FGPositioned* obs)
1280 {
1281   SGVec2d pos = project(obs->geod());
1282   glColor3f(0.0, 0.0, 0.0);
1283   glLineWidth(2.0);
1284   drawLine(pos, pos + SGVec2d());
1285 }
1286 */
1287
1288 void MapWidget::drawAirport(FGAirport* apt)
1289 {
1290         // draw tower location
1291         SGVec2d towerPos = project(apt->getTowerLocation());
1292
1293   if (_cachedZoom <= SHOW_DETAIL_ZOOM) {
1294     glColor3f(1.0, 1.0, 1.0);
1295     glLineWidth(1.0);
1296
1297     drawLine(towerPos + SGVec2d(3, 0), towerPos + SGVec2d(3, 10));
1298     drawLine(towerPos + SGVec2d(-3, 0), towerPos + SGVec2d(-3, 10));
1299     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(-3, 10));
1300     drawLine(towerPos + SGVec2d(6, 20), towerPos + SGVec2d(3, 10));
1301     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(6, 20));
1302   }
1303
1304   if (validDataForKey(apt)) {
1305     setAnchorForKey(apt, towerPos);
1306   } else {
1307     char buffer[1024];
1308     ::snprintf(buffer, 1024, "%s\n%s",
1309       apt->ident().c_str(), apt->name().c_str());
1310
1311     MapData* d = createDataForKey(apt);
1312     d->setText(buffer);
1313     d->setLabel(apt->ident());
1314     d->setPriority(100 + scoreAirportRunways(apt));
1315     d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 6);
1316     d->setAnchor(towerPos);
1317   }
1318
1319   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1320     return;
1321   }
1322
1323   FGRunwayList runways(apt->getRunwaysWithoutReciprocals());
1324     
1325   for (unsigned int r=0; r<runways.size(); ++r) {
1326     drawRunwayPre(runways[r]);
1327   }
1328
1329   for (unsigned int r=0; r<runways.size(); ++r) {
1330     FGRunway* rwy = runways[r];
1331     drawRunway(rwy);
1332
1333     if (rwy->ILS()) {
1334         drawILS(false, rwy);
1335     }
1336     
1337     if (rwy->reciprocalRunway()) {
1338       FGRunway* recip = rwy->reciprocalRunway();
1339       if (recip->ILS()) {
1340         drawILS(false, recip);
1341       }
1342     }
1343   }
1344
1345   for (unsigned int r=0; r<apt->numHelipads(); ++r) {
1346       FGHelipad* hp = apt->getHelipadByIndex(r);
1347       drawHelipad(hp);
1348   }  // of runway iteration
1349
1350 }
1351
1352 int MapWidget::scoreAirportRunways(FGAirport* apt)
1353 {
1354   bool needHardSurface = _root->getBoolValue("hard-surfaced-airports", true);
1355   double minLength = _root->getDoubleValue("min-runway-length-ft", 2000.0);
1356
1357   FGRunwayList runways(apt->getRunwaysWithoutReciprocals());
1358
1359   int score = 0;
1360   for (unsigned int r=0; r<runways.size(); ++r) {
1361     FGRunway* rwy = runways[r];
1362     if (needHardSurface && !rwy->isHardSurface()) {
1363       continue;
1364     }
1365
1366     if (rwy->lengthFt() < minLength) {
1367       continue;
1368     }
1369
1370     int scoreLength = SGMiscd::roundToInt(rwy->lengthFt() / 200.0);
1371     score += scoreLength;
1372   } // of runways iteration
1373
1374   return score;
1375 }
1376
1377 void MapWidget::drawRunwayPre(FGRunway* rwy)
1378 {
1379   SGVec2d p1 = project(rwy->begin());
1380         SGVec2d p2 = project(rwy->end());
1381
1382   glLineWidth(4.0);
1383   glColor3f(1.0, 0.0, 1.0);
1384         drawLine(p1, p2);
1385 }
1386
1387 void MapWidget::drawRunway(FGRunway* rwy)
1388 {
1389         // line for runway
1390         // optionally show active, stopway, etc
1391         // in legend, show published heading and length
1392         // and threshold elevation
1393
1394   SGVec2d p1 = project(rwy->begin());
1395         SGVec2d p2 = project(rwy->end());
1396   glLineWidth(2.0);
1397   glColor3f(1.0, 1.0, 1.0);
1398   SGVec2d inset = normalize(p2 - p1) * 2;
1399
1400         drawLine(p1 + inset, p2 - inset);
1401
1402   if (validDataForKey(rwy)) {
1403     setAnchorForKey(rwy, (p1 + p2) * 0.5);
1404     return;
1405   }
1406   
1407         char buffer[1024];
1408         ::snprintf(buffer, 1024, "%s/%s\n%03d/%03d\n%.0f'",
1409                 rwy->ident().c_str(),
1410                 rwy->reciprocalRunway()->ident().c_str(),
1411                 displayHeading(rwy->headingDeg()),
1412                 displayHeading(rwy->reciprocalRunway()->headingDeg()),
1413                 rwy->lengthFt());
1414
1415   MapData* d = createDataForKey(rwy);
1416   d->setText(buffer);
1417   d->setLabel(rwy->ident() + "/" + rwy->reciprocalRunway()->ident());
1418   d->setPriority(50);
1419   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1420   d->setAnchor((p1 + p2) * 0.5);
1421 }
1422
1423 void MapWidget::drawILS(bool tuned, FGRunway* rwy)
1424 {
1425         // arrow, tip centered on the landing threshold
1426   // using LOC transmitter position would be more accurate, but
1427   // is visually cluttered
1428         // arrow width is based upon the computed localizer width
1429
1430         FGNavRecord* loc = rwy->ILS();
1431         double halfBeamWidth = loc->localizerWidth() * 0.5;
1432         SGVec2d t = project(rwy->threshold());
1433         SGGeod locEnd;
1434         double rangeM = loc->get_range() * SG_NM_TO_METER;
1435         double radial = loc->get_multiuse();
1436   SG_NORMALIZE_RANGE(radial, 0.0, 360.0);
1437         double az2;
1438
1439 // compute the three end points at the widge end of the arrow
1440         SGGeodesy::direct(loc->geod(), radial, -rangeM, locEnd, az2);
1441         SGVec2d endCentre = project(locEnd);
1442
1443         SGGeodesy::direct(loc->geod(), radial + halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1444         SGVec2d endR = project(locEnd);
1445
1446         SGGeodesy::direct(loc->geod(), radial - halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1447         SGVec2d endL = project(locEnd);
1448
1449 // outline two triangles
1450   glLineWidth(1.0);
1451   if (tuned) {
1452     glColor3f(0.0, 1.0, 1.0);
1453   } else {
1454     glColor3f(0.0, 0.0, 1.0);
1455         }
1456
1457   glBegin(GL_LINE_LOOP);
1458                 glVertex2dv(t.data());
1459                 glVertex2dv(endCentre.data());
1460                 glVertex2dv(endL.data());
1461         glEnd();
1462         glBegin(GL_LINE_LOOP);
1463                 glVertex2dv(t.data());
1464                 glVertex2dv(endCentre.data());
1465                 glVertex2dv(endR.data());
1466         glEnd();
1467
1468         if (validDataForKey(loc)) {
1469     setAnchorForKey(loc, endR);
1470     return;
1471   }
1472
1473         char buffer[1024];
1474         ::snprintf(buffer, 1024, "%s\n%s\n%03d - %3.2fMHz",
1475                 loc->ident().c_str(), loc->name().c_str(),
1476     displayHeading(radial),
1477     loc->get_freq()/100.0);
1478
1479   MapData* d = createDataForKey(loc);
1480   d->setPriority(40);
1481   d->setLabel(loc->ident());
1482   d->setText(buffer);
1483   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1484   d->setAnchor(endR);
1485 }
1486
1487 void MapWidget::drawTraffic()
1488 {
1489   if (!_root->getBoolValue("draw-traffic")) {
1490     return;
1491   }
1492
1493   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1494     return;
1495   }
1496
1497   const SGPropertyNode* ai = fgGetNode("/ai/models", true);
1498
1499   for (int i = 0; i < ai->nChildren(); ++i) {
1500     const SGPropertyNode *model = ai->getChild(i);
1501     // skip bad or dead entries
1502     if (!model || model->getIntValue("id", -1) == -1) {
1503       continue;
1504     }
1505
1506     const std::string& name(model->getName());
1507     SGGeod pos = SGGeod::fromDegFt(
1508       model->getDoubleValue("position/longitude-deg"),
1509       model->getDoubleValue("position/latitude-deg"),
1510       model->getDoubleValue("position/altitude-ft"));
1511
1512     double dist = SGGeodesy::distanceNm(_projectionCenter, pos);
1513     if (dist > _drawRangeNm) {
1514       continue;
1515     }
1516
1517     double heading = model->getDoubleValue("orientation/true-heading-deg");
1518     if ((name == "aircraft") || (name == "multiplayer") ||
1519         (name == "wingman") || (name == "tanker")) {
1520       drawAIAircraft(model, pos, heading);
1521     } else if ((name == "ship") || (name == "carrier") || (name == "escort")) {
1522       drawAIShip(model, pos, heading);
1523     }
1524   } // of ai/models iteration
1525 }
1526
1527 void MapWidget::drawHelipad(FGHelipad* hp)
1528 {
1529   SGVec2d pos = project(hp->geod());
1530   glLineWidth(1.0);
1531   glColor3f(1.0, 0.0, 1.0);
1532   circleAt(pos, 16, 5.0);
1533
1534   if (validDataForKey(hp)) {
1535     setAnchorForKey(hp, pos);
1536     return;
1537   }
1538
1539   char buffer[1024];
1540   ::snprintf(buffer, 1024, "%s\n%03d\n%.0f'",
1541              hp->ident().c_str(),
1542              displayHeading(hp->headingDeg()),
1543              hp->lengthFt());
1544
1545   MapData* d = createDataForKey(hp);
1546   d->setText(buffer);
1547   d->setLabel(hp->ident());
1548   d->setPriority(40);
1549   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 8);
1550   d->setAnchor(pos);
1551 }
1552
1553 void MapWidget::drawAIAircraft(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1554 {
1555
1556   SGVec2d p = project(pos);
1557
1558   glColor3f(0.0, 0.0, 0.0);
1559   glLineWidth(2.0);
1560   circleAt(p, 4, 6.0); // black diamond
1561
1562 // draw heading vector
1563   int speedKts = static_cast<int>(model->getDoubleValue("velocities/true-airspeed-kt"));
1564   if (speedKts > 1) {
1565     glLineWidth(1.0);
1566
1567     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1568     double distanceM = speedKts * SG_NM_TO_METER * dt;
1569
1570     SGGeod advance;
1571     double az2;
1572     SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1573
1574     drawLine(p, project(advance));
1575   }
1576    
1577     // try to access the flight-plan of the aircraft. There are several layers
1578     // of potential NULL-ness here, so we have to be defensive at each stage.
1579     std::string originICAO, destinationICAO;
1580     FGAIManager* aiManager = static_cast<FGAIManager*>(globals->get_subsystem("ai-model"));
1581     FGAIBasePtr aircraft = aiManager ? aiManager->getObjectFromProperty(model) : NULL;
1582     if (aircraft) {
1583         FGAIAircraft* p = static_cast<FGAIAircraft*>(aircraft.get());
1584         if (p->GetFlightPlan()) {
1585             if (p->GetFlightPlan()->departureAirport()) {
1586                 originICAO = p->GetFlightPlan()->departureAirport()->ident();
1587             }
1588             
1589             if (p->GetFlightPlan()->arrivalAirport()) {
1590                 destinationICAO = p->GetFlightPlan()->arrivalAirport()->ident();
1591             }
1592         } // of flight-plan exists
1593     } // of check for AIBase-derived instance
1594
1595   // draw callsign / altitude / speed
1596     int altFt50 = static_cast<int>(pos.getElevationFt() / 50.0) * 50;
1597     std::ostringstream ss;
1598     ss << model->getStringValue("callsign", "<>");
1599     if (speedKts > 1) {
1600         ss << "\n" << altFt50 << "' " << speedKts << "kts";
1601     }
1602     
1603     if (!originICAO.empty() || ! destinationICAO.empty()) {
1604         ss << "\n" << originICAO << " -> " << destinationICAO;
1605     }
1606     
1607     MapData* d = getOrCreateDataForKey((void*) model);
1608     d->setText(ss.str().c_str());
1609     d->setLabel(model->getStringValue("callsign", "<>"));
1610     d->setPriority(speedKts > 5 ? 60 : 10); // low priority for parked aircraft
1611     d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1612     d->setAnchor(p);
1613 }
1614
1615 void MapWidget::drawAIShip(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1616 {
1617   SGVec2d p = project(pos);
1618
1619   glColor3f(0.0, 0.0, 0.5);
1620   glLineWidth(2.0);
1621   circleAt(p, 4, 6.0); // blue diamond (to differentiate from aircraft.
1622
1623 // draw heading vector
1624   int speedKts = static_cast<int>(model->getDoubleValue("velocities/speed-kts"));
1625   if (speedKts > 1) {
1626     glLineWidth(1.0);
1627
1628     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1629     double distanceM = speedKts * SG_NM_TO_METER * dt;
1630
1631     SGGeod advance;
1632     double az2;
1633     SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1634
1635     drawLine(p, project(advance));
1636   }
1637
1638   // draw callsign / speed
1639   char buffer[1024];
1640         ::snprintf(buffer, 1024, "%s\n%dkts",
1641                 model->getStringValue("name", "<>"),
1642     speedKts);
1643
1644   MapData* d = getOrCreateDataForKey((void*) model);
1645   d->setText(buffer);
1646   d->setLabel(model->getStringValue("name", "<>"));
1647   d->setPriority(speedKts > 2 ? 30 : 10); // low priority for slow moving ships
1648   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1649   d->setAnchor(p);
1650 }
1651
1652 SGVec2d MapWidget::project(const SGGeod& geod) const
1653 {
1654   SGVec2d p;
1655   double r = earth_radius_lat(geod.getLatitudeRad());
1656   
1657     switch (_projection) {
1658     case PROJECTION_SAMSON_FLAMSTEED:
1659     {
1660         // Sanson-Flamsteed projection, relative to the projection center
1661         double lonDiff = geod.getLongitudeRad() - _projectionCenter.getLongitudeRad(),
1662         latDiff = geod.getLatitudeRad() - _projectionCenter.getLatitudeRad();
1663         
1664         p = SGVec2d(cos(geod.getLatitudeRad()) * lonDiff, latDiff) * r * currentScale();
1665         break;
1666     }
1667             
1668     case PROJECTION_ORTHO_AZIMUTH:
1669     {
1670         // http://mathworld.wolfram.com/OrthographicProjection.html
1671         double cosTheta = cos(geod.getLatitudeRad());
1672         double sinDLambda = sin(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1673         double cosDLambda = cos(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1674         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1675         double sinTheta = sin(geod.getLatitudeRad());
1676         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1677         
1678         p = SGVec2d(cosTheta * sinDLambda,
1679                     (cosTheta1 * sinTheta) - (sinTheta1 * cosTheta * cosDLambda)) * r * currentScale();
1680         break;
1681     }
1682             
1683     case PROJECTION_SPHERICAL:
1684     {
1685         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1686         SGVec3d cartPt = SGVec3d::fromGeod(geod) - cartCenter;
1687         
1688         // rotate relative to projection center
1689         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1690         cartPt = orient.rotateBack(cartPt);
1691         return SGVec2d(cartPt.y(), cartPt.x()) * currentScale();
1692         break;
1693     }
1694     } // of projection mode switch
1695     
1696   
1697 // rotate as necessary
1698   double cost = cos(_upHeading * SG_DEGREES_TO_RADIANS),
1699     sint = sin(_upHeading * SG_DEGREES_TO_RADIANS);
1700   double rx = cost * p.x() - sint * p.y();
1701   double ry = sint * p.x() + cost * p.y();
1702   return SGVec2d(rx, ry);
1703 }
1704
1705 SGGeod MapWidget::unproject(const SGVec2d& p) const
1706 {
1707   // unrotate, if necessary
1708   double cost = cos(-_upHeading * SG_DEGREES_TO_RADIANS),
1709     sint = sin(-_upHeading * SG_DEGREES_TO_RADIANS);
1710   SGVec2d ur(cost * p.x() - sint * p.y(),
1711              sint * p.x() + cost * p.y());
1712
1713   
1714
1715     switch (_projection) {
1716     case PROJECTION_SAMSON_FLAMSTEED:
1717     {
1718         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1719         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1720         double lat = unscaled.y() + _projectionCenter.getLatitudeRad();
1721         double lon = (unscaled.x() / cos(lat)) + _projectionCenter.getLongitudeRad();
1722         return SGGeod::fromRad(lon, lat);
1723     }
1724         
1725     case PROJECTION_ORTHO_AZIMUTH:
1726     {
1727         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1728         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1729         
1730         double phi = length(p);
1731         double c = asin(phi);
1732         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1733         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1734         
1735         double lat = asin(cos(c) * sinTheta1 + ((unscaled.y() * sin(c) * cosTheta1) / phi));
1736         double lon = _projectionCenter.getLongitudeRad() +
1737         atan((unscaled.x()* sin(c)) / (phi  * cosTheta1 * cos(c) - unscaled.y() * sinTheta1 * sin(c)));
1738         return SGGeod::fromRad(lon, lat);
1739     }
1740         
1741     case PROJECTION_SPHERICAL:
1742     {
1743         SGVec2d unscaled = ur * (1.0 / currentScale());
1744         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1745         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1746         SGVec3d cartPt = orient.rotate(SGVec3d(unscaled.x(), unscaled.y(), 0.0));
1747         return SGGeod::fromCart(cartPt + cartCenter);
1748     }
1749     } // of projection mode switch
1750 }
1751
1752 double MapWidget::currentScale() const
1753 {
1754   return 1.0 / pow(2.0, _cachedZoom);
1755 }
1756
1757 void MapWidget::circleAt(const SGVec2d& center, int nSides, double r)
1758 {
1759   glBegin(GL_LINE_LOOP);
1760   double advance = (SGD_PI * 2) / nSides;
1761   glVertex2d(center.x() +r, center.y());
1762   double t=advance;
1763   for (int i=1; i<nSides; ++i) {
1764     glVertex2d(center.x() + (cos(t) * r), center.y() + (sin(t) * r));
1765     t += advance;
1766   }
1767   glEnd();
1768 }
1769
1770 void MapWidget::squareAt(const SGVec2d& center, double r)
1771 {
1772   glBegin(GL_LINE_LOOP);
1773   glVertex2d(center.x() + r, center.y() + r);
1774   glVertex2d(center.x() + r, center.y() - r);
1775   glVertex2d(center.x() - r, center.y() - r);
1776   glVertex2d(center.x() - r, center.y() + r);
1777   glEnd();
1778 }
1779
1780 void MapWidget::circleAtAlt(const SGVec2d& center, int nSides, double r, double r2)
1781 {
1782   glBegin(GL_LINE_LOOP);
1783   double advance = (SGD_PI * 2) / nSides;
1784   glVertex2d(center.x(), center.y() + r);
1785   double t=advance;
1786   for (int i=1; i<nSides; ++i) {
1787     double rr = (i%2 == 0) ? r : r2;
1788     glVertex2d(center.x() + (sin(t) * rr), center.y() + (cos(t) * rr));
1789     t += advance;
1790   }
1791   glEnd();
1792 }
1793
1794 void MapWidget::drawLine(const SGVec2d& p1, const SGVec2d& p2)
1795 {
1796   glBegin(GL_LINES);
1797     glVertex2dv(p1.data());
1798     glVertex2dv(p2.data());
1799   glEnd();
1800 }
1801
1802 void MapWidget::drawLegendBox(const SGVec2d& pos, const std::string& t)
1803 {
1804         std::vector<std::string> lines(simgear::strutils::split(t, "\n"));
1805         const int LINE_LEADING = 4;
1806         const int MARGIN = 4;
1807
1808 // measure
1809         int maxWidth = -1, totalHeight = 0;
1810         int lineHeight = legendFont.getStringHeight();
1811
1812         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1813                 totalHeight += lineHeight;
1814                 if (ln > 0) {
1815                         totalHeight += LINE_LEADING;
1816                 }
1817
1818                 int lw = legendFont.getStringWidth(lines[ln].c_str());
1819                 maxWidth = std::max(maxWidth, lw);
1820         } // of line measurement
1821
1822         if (maxWidth < 0) {
1823                 return; // all lines are empty, don't draw
1824         }
1825
1826         totalHeight += MARGIN * 2;
1827
1828 // draw box
1829         puBox box;
1830         box.min[0] = 0;
1831         box.min[1] = -totalHeight;
1832         box.max[0] = maxWidth + (MARGIN * 2);
1833         box.max[1] = 0;
1834         int border = 1;
1835         box.draw (pos.x(), pos.y(), PUSTYLE_DROPSHADOW, colour, FALSE, border);
1836
1837 // draw lines
1838         int xPos = pos.x() + MARGIN;
1839         int yPos = pos.y() - (lineHeight + MARGIN);
1840         glColor3f(0.8, 0.8, 0.8);
1841
1842         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1843                 legendFont.drawString(lines[ln].c_str(), xPos, yPos);
1844                 yPos -= lineHeight + LINE_LEADING;
1845         }
1846 }
1847
1848 void MapWidget::drawData()
1849 {
1850   std::sort(_dataQueue.begin(), _dataQueue.end(), MapData::order);
1851
1852   int hw = _width >> 1,
1853     hh = _height >> 1;
1854   puBox visBox(makePuBox(-hw, -hh, _width, _height));
1855
1856   unsigned int d = 0;
1857   int drawn = 0;
1858   std::vector<MapData*> drawQueue;
1859
1860   bool drawData = _root->getBoolValue("draw-data");
1861   const int MAX_DRAW_DATA = 25;
1862   const int MAX_DRAW = 50;
1863
1864   for (; (d < _dataQueue.size()) && (drawn < MAX_DRAW); ++d) {
1865     MapData* md = _dataQueue[d];
1866     md->setDataVisible(drawData);
1867
1868     if (md->isClipped(visBox)) {
1869       continue;
1870     }
1871
1872     if (md->overlaps(drawQueue)) {
1873       if (drawData) { // overlapped with data, let's try just the label
1874         md->setDataVisible(false);
1875         if (md->overlaps(drawQueue)) {
1876           continue;
1877         }
1878       } else {
1879         continue;
1880       }
1881     } // of overlaps case
1882
1883     drawQueue.push_back(md);
1884     ++drawn;
1885     if (drawData && (drawn >= MAX_DRAW_DATA)) {
1886       drawData = false;
1887     }
1888   }
1889
1890   // draw lowest-priority first, so higher-priorty items appear on top
1891   std::vector<MapData*>::reverse_iterator r;
1892   for (r = drawQueue.rbegin(); r!= drawQueue.rend(); ++r) {
1893     (*r)->draw();
1894   }
1895
1896   _dataQueue.clear();
1897   KeyDataMap::iterator it = _mapData.begin();
1898   for (; it != _mapData.end(); ) {
1899     it->second->age();
1900     if (it->second->isExpired()) {
1901       delete it->second;
1902       KeyDataMap::iterator cur = it++;
1903       _mapData.erase(cur);
1904     } else {
1905       ++it;
1906     }
1907   } // of expiry iteration
1908 }
1909
1910 bool MapWidget::validDataForKey(void* key)
1911 {
1912   KeyDataMap::iterator it = _mapData.find(key);
1913   if (it == _mapData.end()) {
1914     return false; // no valid data for the key!
1915   }
1916
1917   it->second->resetAge(); // mark data as valid this frame
1918   _dataQueue.push_back(it->second);
1919   return true;
1920 }
1921
1922 void MapWidget::setAnchorForKey(void* key, const SGVec2d& anchor)
1923 {
1924   KeyDataMap::iterator it = _mapData.find(key);
1925   if (it == _mapData.end()) {
1926     throw sg_exception("no valid data for key!");
1927   }
1928
1929   it->second->setAnchor(anchor);
1930 }
1931
1932 MapData* MapWidget::getOrCreateDataForKey(void* key)
1933 {
1934   KeyDataMap::iterator it = _mapData.find(key);
1935   if (it == _mapData.end()) {
1936     return createDataForKey(key);
1937   }
1938
1939   it->second->resetAge(); // mark data as valid this frame
1940   _dataQueue.push_back(it->second);
1941   return it->second;
1942 }
1943
1944 MapData* MapWidget::createDataForKey(void* key)
1945 {
1946   KeyDataMap::iterator it = _mapData.find(key);
1947   if (it != _mapData.end()) {
1948     throw sg_exception("duplicate data requested for key!");
1949   }
1950
1951   MapData* d =  new MapData(0);
1952   _mapData[key] = d;
1953   _dataQueue.push_back(d);
1954   d->resetAge();
1955   return d;
1956 }
1957
1958 void MapWidget::clearData()
1959 {
1960   KeyDataMap::iterator it = _mapData.begin();
1961   for (; it != _mapData.end(); ++it) {
1962     delete it->second;
1963   }
1964   
1965   _mapData.clear();
1966 }
1967
1968 int MapWidget::displayHeading(double h) const
1969 {
1970   if (_magneticHeadings) {
1971     h -= _magVar->get_magvar() * SG_RADIANS_TO_DEGREES;
1972   }
1973   
1974   SG_NORMALIZE_RANGE(h, 0.0, 360.0);
1975   return SGMiscd::roundToInt(h);
1976 }