Display AI traffic route in map.
[fg: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 private:
935   bool _heliports;
936   bool _hardRunwaysOnly;
937   double _minLengthFt;
938 };
939
940 void MapWidget::drawAirports()
941 {
942   MapAirportFilter af(_root);
943   bool partial = false;
944   FGPositionedList apts = FGPositioned::findWithinRangePartial(_projectionCenter, _drawRangeNm, &af, partial);
945   for (unsigned int i=0; i<apts.size(); ++i) {
946     drawAirport((FGAirport*) apts[i].get());
947   }
948 }
949
950 class NavaidFilter : public FGPositioned::Filter
951 {
952 public:
953   NavaidFilter(bool fixesEnabled, bool navaidsEnabled) :
954     _fixes(fixesEnabled),
955     _navaids(navaidsEnabled)
956   {}
957
958   virtual bool pass(FGPositioned* aPos) const {
959     if (_fixes && (aPos->type() == FGPositioned::FIX)) {
960       // ignore fixes which end in digits - expirmental
961       if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
962         return false;
963       }
964     }
965
966     return true;
967   }
968
969   virtual FGPositioned::Type minType() const {
970     return _fixes ? FGPositioned::FIX : FGPositioned::NDB;
971   }
972
973   virtual FGPositioned::Type maxType() const {
974     return _navaids ? FGPositioned::VOR : FGPositioned::FIX;
975   }
976
977 private:
978   bool _fixes, _navaids;
979 };
980
981 void MapWidget::drawNavaids()
982 {
983   bool fixes = _root->getBoolValue("draw-fixes");
984   NavaidFilter f(fixes, _root->getBoolValue("draw-navaids"));
985
986   if (f.minType() <= f.maxType()) {
987     FGPositionedList navs = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f);
988
989     glLineWidth(1.0);
990     for (unsigned int i=0; i<navs.size(); ++i) {
991       FGPositioned::Type ty = navs[i]->type();
992       if (ty == FGPositioned::NDB) {
993         drawNDB(false, (FGNavRecord*) navs[i].get());
994       } else if (ty == FGPositioned::VOR) {
995         drawVOR(false, (FGNavRecord*) navs[i].get());
996       } else if (ty == FGPositioned::FIX) {
997         drawFix((FGFix*) navs[i].get());
998       }
999     } // of navaid iteration
1000   } // of navaids || fixes are drawn test
1001 }
1002
1003 void MapWidget::drawPOIs()
1004 {
1005   FGPositioned::TypeFilter f(FGPositioned::COUNTRY);
1006   f.addType(FGPositioned::CITY);
1007   f.addType(FGPositioned::TOWN);
1008   FGPositionedList poi = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f);
1009
1010     glLineWidth(1.0);
1011     for (unsigned int i=0; i<poi.size(); ++i) {
1012       FGPositioned::Type ty = poi[i]->type();
1013       if (ty == FGPositioned::COUNTRY) {
1014         drawCountries((FGNavRecord*) poi[i].get());
1015       } else if (ty == FGPositioned::CITY) {
1016         drawCities((FGNavRecord*) poi[i].get());
1017       } else if (ty == FGPositioned::TOWN) {
1018         drawTowns((FGNavRecord*) poi[i].get());
1019       }
1020     } // of navaid iteration
1021 }
1022
1023 void MapWidget::drawNDB(bool tuned, FGNavRecord* ndb)
1024 {
1025   SGVec2d pos = project(ndb->geod());
1026
1027   if (tuned) {
1028     glColor3f(0.0, 1.0, 1.0);
1029   } else {
1030     glColor3f(0.0, 0.0, 0.0);
1031   }
1032
1033   glEnable(GL_LINE_STIPPLE);
1034   glLineStipple(1, 0x00FF);
1035   circleAt(pos, 20, 6);
1036   circleAt(pos, 20, 10);
1037   glDisable(GL_LINE_STIPPLE);
1038
1039   if (validDataForKey(ndb)) {
1040     setAnchorForKey(ndb, pos);
1041     return;
1042   }
1043
1044   char buffer[1024];
1045         ::snprintf(buffer, 1024, "%s\n%s %3.0fKhz",
1046                 ndb->name().c_str(), ndb->ident().c_str(),ndb->get_freq()/100.0);
1047
1048   MapData* d = createDataForKey(ndb);
1049   d->setPriority(40);
1050   d->setLabel(ndb->ident());
1051   d->setText(buffer);
1052   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1053   d->setAnchor(pos);
1054
1055 }
1056
1057 void MapWidget::drawVOR(bool tuned, FGNavRecord* vor)
1058 {
1059   SGVec2d pos = project(vor->geod());
1060   if (tuned) {
1061     glColor3f(0.0, 1.0, 1.0);
1062   } else {
1063     glColor3f(0.0, 0.0, 1.0);
1064   }
1065
1066   circleAt(pos, 6, 9);
1067   circleAt(pos, 8, 1);
1068
1069   if (vor->hasDME())
1070   squareAt(pos, 9);
1071
1072   if (validDataForKey(vor)) {
1073     setAnchorForKey(vor, pos);
1074     return;
1075   }
1076
1077   char buffer[1024];
1078         ::snprintf(buffer, 1024, "%s\n%s %6.3fMhz",
1079                 vor->name().c_str(), vor->ident().c_str(),
1080     vor->get_freq() / 100.0);
1081
1082   MapData* d = createDataForKey(vor);
1083   d->setText(buffer);
1084   d->setLabel(vor->ident());
1085   d->setPriority(tuned ? 10000 : 100);
1086   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1087   d->setAnchor(pos);
1088 }
1089
1090 void MapWidget::drawFix(FGFix* fix)
1091 {
1092   SGVec2d pos = project(fix->geod());
1093   glColor3f(0.0, 0.0, 0.0);
1094   circleAt(pos, 3, 6);
1095
1096   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1097     return; // hide fix labels beyond a certain zoom level
1098   }
1099
1100   if (validDataForKey(fix)) {
1101     setAnchorForKey(fix, pos);
1102     return;
1103   }
1104
1105   MapData* d = createDataForKey(fix);
1106   d->setLabel(fix->ident());
1107   d->setPriority(20);
1108   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1109   d->setAnchor(pos);
1110 }
1111
1112 void MapWidget::drawNavRadio(SGPropertyNode_ptr radio)
1113 {
1114   if (!radio || radio->getBoolValue("slaved-to-gps", false)
1115         || !radio->getBoolValue("in-range", false)) {
1116     return;
1117   }
1118
1119   if (radio->getBoolValue("nav-loc", false)) {
1120     drawTunedLocalizer(radio);
1121   }
1122
1123   // identify the tuned station - unfortunately we don't get lat/lon directly,
1124   // need to do the frequency search again
1125   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1126
1127   FGNavRecord* nav = FGNavList::findByFreq(mhz, _aircraft,
1128                                            FGNavList::navFilter());
1129   if (!nav || (nav->ident() != radio->getStringValue("nav-id"))) {
1130     // mismatch between navradio selection logic and ours!
1131     return;
1132   }
1133
1134   glLineWidth(1.0);
1135   drawVOR(true, nav);
1136
1137   SGVec2d pos = project(nav->geod());
1138   SGGeod range;
1139   double az2;
1140   double trueRadial = radio->getDoubleValue("radials/target-radial-deg");
1141   SGGeodesy::direct(nav->geod(), trueRadial, nav->get_range() * SG_NM_TO_METER, range, az2);
1142   SGVec2d prange = project(range);
1143
1144   SGVec2d norm = normalize(prange - pos);
1145   SGVec2d perp(norm.y(), -norm.x());
1146
1147   circleAt(pos, 64, length(prange - pos));
1148   drawLine(pos, prange);
1149
1150 // draw to/from arrows
1151   SGVec2d midPoint = (pos + prange) * 0.5;
1152   if (radio->getBoolValue("from-flag")) {
1153     norm = -norm;
1154     perp = -perp;
1155   }
1156
1157   int sz = 10;
1158   SGVec2d arrowB = midPoint - (norm * sz) + (perp * sz);
1159   SGVec2d arrowC = midPoint - (norm * sz) - (perp * sz);
1160   drawLine(midPoint, arrowB);
1161   drawLine(arrowB, arrowC);
1162   drawLine(arrowC, midPoint);
1163
1164   drawLine(pos, (2 * pos) - prange); // reciprocal radial
1165 }
1166
1167 void MapWidget::drawTunedLocalizer(SGPropertyNode_ptr radio)
1168 {
1169   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1170   FGNavRecord* loc = FGNavList::findByFreq(mhz, _aircraft, FGNavList::locFilter());
1171   if (!loc || (loc->ident() != radio->getStringValue("nav-id"))) {
1172     // mismatch between navradio selection logic and ours!
1173     return;
1174   }
1175
1176   if (loc->runway()) {
1177     drawILS(true, loc->runway());
1178   }
1179 }
1180
1181 void MapWidget::drawCountries(FGNavRecord* rec)
1182 {
1183   if (_cachedZoom < 9) {
1184     return; // hide labels beyond a certain zoom level
1185   }
1186
1187   SGVec2d pos = project(rec->geod());
1188   glColor3f(1.0, 1.0, 0.0);
1189
1190   circleAt(pos, 4, 10);
1191
1192   if (validDataForKey(rec)) {
1193     setAnchorForKey(rec, pos);
1194     return;
1195   }
1196
1197   char buffer[1024];
1198         ::snprintf(buffer, 1024, "%s",
1199                 rec->name().c_str());
1200
1201   MapData* d = createDataForKey(rec);
1202   d->setPriority(200);
1203   d->setLabel(rec->ident());
1204   d->setText(buffer);
1205   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1206   d->setAnchor(pos);
1207
1208 }
1209
1210 void MapWidget::drawCities(FGNavRecord* rec)
1211 {
1212   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1213     return; // hide labels beyond a certain zoom level
1214   }
1215
1216   SGVec2d pos = project(rec->geod());
1217   glColor3f(0.0, 1.0, 0.0);
1218
1219   circleAt(pos, 4, 8);
1220
1221   if (validDataForKey(rec)) {
1222     setAnchorForKey(rec, pos);
1223     return;
1224   }
1225
1226   char buffer[1024];
1227         ::snprintf(buffer, 1024, "%s",
1228                 rec->name().c_str());
1229
1230   MapData* d = createDataForKey(rec);
1231   d->setPriority(40);
1232   d->setLabel(rec->ident());
1233   d->setText(buffer);
1234   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1235   d->setAnchor(pos);
1236
1237 }
1238
1239 void MapWidget::drawTowns(FGNavRecord* rec)
1240 {
1241   if (_cachedZoom > SHOW_DETAIL2_ZOOM) {
1242     return; // hide labels beyond a certain zoom level
1243   }
1244
1245   SGVec2d pos = project(rec->geod());
1246   glColor3f(0.2, 1.0, 0.0);
1247
1248   circleAt(pos, 4, 5);
1249
1250   if (validDataForKey(rec)) {
1251     setAnchorForKey(rec, pos);
1252     return;
1253   }
1254
1255   char buffer[1024];
1256         ::snprintf(buffer, 1024, "%s",
1257                 rec->name().c_str());
1258
1259   MapData* d = createDataForKey(rec);
1260   d->setPriority(40);
1261   d->setLabel(rec->ident());
1262   d->setText(buffer);
1263   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1264   d->setAnchor(pos);
1265
1266 }
1267
1268 /*
1269 void MapWidget::drawObstacle(FGPositioned* obs)
1270 {
1271   SGVec2d pos = project(obs->geod());
1272   glColor3f(0.0, 0.0, 0.0);
1273   glLineWidth(2.0);
1274   drawLine(pos, pos + SGVec2d());
1275 }
1276 */
1277
1278 void MapWidget::drawAirport(FGAirport* apt)
1279 {
1280         // draw tower location
1281         SGVec2d towerPos = project(apt->getTowerLocation());
1282
1283   if (_cachedZoom <= SHOW_DETAIL_ZOOM) {
1284     glColor3f(1.0, 1.0, 1.0);
1285     glLineWidth(1.0);
1286
1287     drawLine(towerPos + SGVec2d(3, 0), towerPos + SGVec2d(3, 10));
1288     drawLine(towerPos + SGVec2d(-3, 0), towerPos + SGVec2d(-3, 10));
1289     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(-3, 10));
1290     drawLine(towerPos + SGVec2d(6, 20), towerPos + SGVec2d(3, 10));
1291     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(6, 20));
1292   }
1293
1294   if (validDataForKey(apt)) {
1295     setAnchorForKey(apt, towerPos);
1296   } else {
1297     char buffer[1024];
1298     ::snprintf(buffer, 1024, "%s\n%s",
1299       apt->ident().c_str(), apt->name().c_str());
1300
1301     MapData* d = createDataForKey(apt);
1302     d->setText(buffer);
1303     d->setLabel(apt->ident());
1304     d->setPriority(100 + scoreAirportRunways(apt));
1305     d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 6);
1306     d->setAnchor(towerPos);
1307   }
1308
1309   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1310     return;
1311   }
1312
1313   FGRunwayList runways(apt->getRunwaysWithoutReciprocals());
1314     
1315   for (unsigned int r=0; r<runways.size(); ++r) {
1316     drawRunwayPre(runways[r]);
1317   }
1318
1319   for (unsigned int r=0; r<runways.size(); ++r) {
1320     FGRunway* rwy = runways[r];
1321     drawRunway(rwy);
1322
1323     if (rwy->ILS()) {
1324         drawILS(false, rwy);
1325     }
1326     
1327     if (rwy->reciprocalRunway()) {
1328       FGRunway* recip = rwy->reciprocalRunway();
1329       if (recip->ILS()) {
1330         drawILS(false, recip);
1331       }
1332     }
1333   }
1334
1335   for (unsigned int r=0; r<apt->numHelipads(); ++r) {
1336       FGHelipad* hp = apt->getHelipadByIndex(r);
1337       drawHelipad(hp);
1338   }  // of runway iteration
1339
1340 }
1341
1342 int MapWidget::scoreAirportRunways(FGAirport* apt)
1343 {
1344   bool needHardSurface = _root->getBoolValue("hard-surfaced-airports", true);
1345   double minLength = _root->getDoubleValue("min-runway-length-ft", 2000.0);
1346
1347   FGRunwayList runways(apt->getRunwaysWithoutReciprocals());
1348
1349   int score = 0;
1350   for (unsigned int r=0; r<runways.size(); ++r) {
1351     FGRunway* rwy = runways[r];
1352     if (needHardSurface && !rwy->isHardSurface()) {
1353       continue;
1354     }
1355
1356     if (rwy->lengthFt() < minLength) {
1357       continue;
1358     }
1359
1360     int scoreLength = SGMiscd::roundToInt(rwy->lengthFt() / 200.0);
1361     score += scoreLength;
1362   } // of runways iteration
1363
1364   return score;
1365 }
1366
1367 void MapWidget::drawRunwayPre(FGRunway* rwy)
1368 {
1369   SGVec2d p1 = project(rwy->begin());
1370         SGVec2d p2 = project(rwy->end());
1371
1372   glLineWidth(4.0);
1373   glColor3f(1.0, 0.0, 1.0);
1374         drawLine(p1, p2);
1375 }
1376
1377 void MapWidget::drawRunway(FGRunway* rwy)
1378 {
1379         // line for runway
1380         // optionally show active, stopway, etc
1381         // in legend, show published heading and length
1382         // and threshold elevation
1383
1384   SGVec2d p1 = project(rwy->begin());
1385         SGVec2d p2 = project(rwy->end());
1386   glLineWidth(2.0);
1387   glColor3f(1.0, 1.0, 1.0);
1388   SGVec2d inset = normalize(p2 - p1) * 2;
1389
1390         drawLine(p1 + inset, p2 - inset);
1391
1392   if (validDataForKey(rwy)) {
1393     setAnchorForKey(rwy, (p1 + p2) * 0.5);
1394     return;
1395   }
1396   
1397         char buffer[1024];
1398         ::snprintf(buffer, 1024, "%s/%s\n%03d/%03d\n%.0f'",
1399                 rwy->ident().c_str(),
1400                 rwy->reciprocalRunway()->ident().c_str(),
1401                 displayHeading(rwy->headingDeg()),
1402                 displayHeading(rwy->reciprocalRunway()->headingDeg()),
1403                 rwy->lengthFt());
1404
1405   MapData* d = createDataForKey(rwy);
1406   d->setText(buffer);
1407   d->setLabel(rwy->ident() + "/" + rwy->reciprocalRunway()->ident());
1408   d->setPriority(50);
1409   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1410   d->setAnchor((p1 + p2) * 0.5);
1411 }
1412
1413 void MapWidget::drawILS(bool tuned, FGRunway* rwy)
1414 {
1415         // arrow, tip centered on the landing threshold
1416   // using LOC transmitter position would be more accurate, but
1417   // is visually cluttered
1418         // arrow width is based upon the computed localizer width
1419
1420         FGNavRecord* loc = rwy->ILS();
1421         double halfBeamWidth = loc->localizerWidth() * 0.5;
1422         SGVec2d t = project(rwy->threshold());
1423         SGGeod locEnd;
1424         double rangeM = loc->get_range() * SG_NM_TO_METER;
1425         double radial = loc->get_multiuse();
1426   SG_NORMALIZE_RANGE(radial, 0.0, 360.0);
1427         double az2;
1428
1429 // compute the three end points at the widge end of the arrow
1430         SGGeodesy::direct(loc->geod(), radial, -rangeM, locEnd, az2);
1431         SGVec2d endCentre = project(locEnd);
1432
1433         SGGeodesy::direct(loc->geod(), radial + halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1434         SGVec2d endR = project(locEnd);
1435
1436         SGGeodesy::direct(loc->geod(), radial - halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1437         SGVec2d endL = project(locEnd);
1438
1439 // outline two triangles
1440   glLineWidth(1.0);
1441   if (tuned) {
1442     glColor3f(0.0, 1.0, 1.0);
1443   } else {
1444     glColor3f(0.0, 0.0, 1.0);
1445         }
1446
1447   glBegin(GL_LINE_LOOP);
1448                 glVertex2dv(t.data());
1449                 glVertex2dv(endCentre.data());
1450                 glVertex2dv(endL.data());
1451         glEnd();
1452         glBegin(GL_LINE_LOOP);
1453                 glVertex2dv(t.data());
1454                 glVertex2dv(endCentre.data());
1455                 glVertex2dv(endR.data());
1456         glEnd();
1457
1458         if (validDataForKey(loc)) {
1459     setAnchorForKey(loc, endR);
1460     return;
1461   }
1462
1463         char buffer[1024];
1464         ::snprintf(buffer, 1024, "%s\n%s\n%03d - %3.2fMHz",
1465                 loc->ident().c_str(), loc->name().c_str(),
1466     displayHeading(radial),
1467     loc->get_freq()/100.0);
1468
1469   MapData* d = createDataForKey(loc);
1470   d->setPriority(40);
1471   d->setLabel(loc->ident());
1472   d->setText(buffer);
1473   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1474   d->setAnchor(endR);
1475 }
1476
1477 void MapWidget::drawTraffic()
1478 {
1479   if (!_root->getBoolValue("draw-traffic")) {
1480     return;
1481   }
1482
1483   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1484     return;
1485   }
1486
1487   const SGPropertyNode* ai = fgGetNode("/ai/models", true);
1488
1489   for (int i = 0; i < ai->nChildren(); ++i) {
1490     const SGPropertyNode *model = ai->getChild(i);
1491     // skip bad or dead entries
1492     if (!model || model->getIntValue("id", -1) == -1) {
1493       continue;
1494     }
1495
1496     const std::string& name(model->getName());
1497     SGGeod pos = SGGeod::fromDegFt(
1498       model->getDoubleValue("position/longitude-deg"),
1499       model->getDoubleValue("position/latitude-deg"),
1500       model->getDoubleValue("position/altitude-ft"));
1501
1502     double dist = SGGeodesy::distanceNm(_projectionCenter, pos);
1503     if (dist > _drawRangeNm) {
1504       continue;
1505     }
1506
1507     double heading = model->getDoubleValue("orientation/true-heading-deg");
1508     if ((name == "aircraft") || (name == "multiplayer") ||
1509         (name == "wingman") || (name == "tanker")) {
1510       drawAIAircraft(model, pos, heading);
1511     } else if ((name == "ship") || (name == "carrier") || (name == "escort")) {
1512       drawAIShip(model, pos, heading);
1513     }
1514   } // of ai/models iteration
1515 }
1516
1517 void MapWidget::drawHelipad(FGHelipad* hp)
1518 {
1519   SGVec2d pos = project(hp->geod());
1520   glLineWidth(1.0);
1521   glColor3f(1.0, 0.0, 1.0);
1522   circleAt(pos, 16, 5.0);
1523
1524   if (validDataForKey(hp)) {
1525     setAnchorForKey(hp, pos);
1526     return;
1527   }
1528
1529   char buffer[1024];
1530   ::snprintf(buffer, 1024, "%s\n%03d\n%.0f'",
1531              hp->ident().c_str(),
1532              displayHeading(hp->headingDeg()),
1533              hp->lengthFt());
1534
1535   MapData* d = createDataForKey(hp);
1536   d->setText(buffer);
1537   d->setLabel(hp->ident());
1538   d->setPriority(40);
1539   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 8);
1540   d->setAnchor(pos);
1541 }
1542
1543 void MapWidget::drawAIAircraft(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1544 {
1545
1546   SGVec2d p = project(pos);
1547
1548   glColor3f(0.0, 0.0, 0.0);
1549   glLineWidth(2.0);
1550   circleAt(p, 4, 6.0); // black diamond
1551
1552 // draw heading vector
1553   int speedKts = static_cast<int>(model->getDoubleValue("velocities/true-airspeed-kt"));
1554   if (speedKts > 1) {
1555     glLineWidth(1.0);
1556
1557     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1558     double distanceM = speedKts * SG_NM_TO_METER * dt;
1559
1560     SGGeod advance;
1561     double az2;
1562     SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1563
1564     drawLine(p, project(advance));
1565   }
1566    
1567     // try to access the flight-plan of the aircraft. There are several layers
1568     // of potential NULL-ness here, so we have to be defensive at each stage.
1569     std::string originICAO, destinationICAO;
1570     FGAIManager* aiManager = static_cast<FGAIManager*>(globals->get_subsystem("ai-model"));
1571     FGAIBasePtr aircraft = aiManager ? aiManager->getObjectFromProperty(model) : NULL;
1572     if (aircraft) {
1573         FGAIAircraft* p = static_cast<FGAIAircraft*>(aircraft.get());
1574         if (p->GetFlightPlan()) {
1575             originICAO = p->GetFlightPlan()->departureAirport()->ident();
1576             destinationICAO = p->GetFlightPlan()->arrivalAirport()->ident();
1577         }
1578     }
1579
1580   // draw callsign / altitude / speed
1581     int altFt50 = static_cast<int>(pos.getElevationFt() / 50.0) * 50;
1582     std::ostringstream ss;
1583     ss << model->getStringValue("callsign", "<>");
1584     if (speedKts > 1) {
1585         ss << "\n" << altFt50 << "' " << speedKts << "kts";
1586     }
1587     
1588     if (!originICAO.empty() || ! destinationICAO.empty()) {
1589         ss << "\n" << originICAO << " -> " << destinationICAO;
1590     }
1591     
1592     MapData* d = getOrCreateDataForKey((void*) model);
1593     d->setText(ss.str().c_str());
1594     d->setLabel(model->getStringValue("callsign", "<>"));
1595     d->setPriority(speedKts > 5 ? 60 : 10); // low priority for parked aircraft
1596     d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1597     d->setAnchor(p);
1598 }
1599
1600 void MapWidget::drawAIShip(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1601 {
1602   SGVec2d p = project(pos);
1603
1604   glColor3f(0.0, 0.0, 0.5);
1605   glLineWidth(2.0);
1606   circleAt(p, 4, 6.0); // blue diamond (to differentiate from aircraft.
1607
1608 // draw heading vector
1609   int speedKts = static_cast<int>(model->getDoubleValue("velocities/speed-kts"));
1610   if (speedKts > 1) {
1611     glLineWidth(1.0);
1612
1613     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1614     double distanceM = speedKts * SG_NM_TO_METER * dt;
1615
1616     SGGeod advance;
1617     double az2;
1618     SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1619
1620     drawLine(p, project(advance));
1621   }
1622
1623   // draw callsign / speed
1624   char buffer[1024];
1625         ::snprintf(buffer, 1024, "%s\n%dkts",
1626                 model->getStringValue("name", "<>"),
1627     speedKts);
1628
1629   MapData* d = getOrCreateDataForKey((void*) model);
1630   d->setText(buffer);
1631   d->setLabel(model->getStringValue("name", "<>"));
1632   d->setPriority(speedKts > 2 ? 30 : 10); // low priority for slow moving ships
1633   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1634   d->setAnchor(p);
1635 }
1636
1637 SGVec2d MapWidget::project(const SGGeod& geod) const
1638 {
1639   SGVec2d p;
1640   double r = earth_radius_lat(geod.getLatitudeRad());
1641   
1642     switch (_projection) {
1643     case PROJECTION_SAMSON_FLAMSTEED:
1644     {
1645         // Sanson-Flamsteed projection, relative to the projection center
1646         double lonDiff = geod.getLongitudeRad() - _projectionCenter.getLongitudeRad(),
1647         latDiff = geod.getLatitudeRad() - _projectionCenter.getLatitudeRad();
1648         
1649         p = SGVec2d(cos(geod.getLatitudeRad()) * lonDiff, latDiff) * r * currentScale();
1650         break;
1651     }
1652             
1653     case PROJECTION_ORTHO_AZIMUTH:
1654     {
1655         // http://mathworld.wolfram.com/OrthographicProjection.html
1656         double cosTheta = cos(geod.getLatitudeRad());
1657         double sinDLambda = sin(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1658         double cosDLambda = cos(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1659         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1660         double sinTheta = sin(geod.getLatitudeRad());
1661         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1662         
1663         p = SGVec2d(cosTheta * sinDLambda,
1664                     (cosTheta1 * sinTheta) - (sinTheta1 * cosTheta * cosDLambda)) * r * currentScale();
1665         break;
1666     }
1667             
1668     case PROJECTION_SPHERICAL:
1669     {
1670         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1671         SGVec3d cartPt = SGVec3d::fromGeod(geod) - cartCenter;
1672         
1673         // rotate relative to projection center
1674         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1675         cartPt = orient.rotateBack(cartPt);
1676         return SGVec2d(cartPt.y(), cartPt.x()) * currentScale();
1677         break;
1678     }
1679     } // of projection mode switch
1680     
1681   
1682 // rotate as necessary
1683   double cost = cos(_upHeading * SG_DEGREES_TO_RADIANS),
1684     sint = sin(_upHeading * SG_DEGREES_TO_RADIANS);
1685   double rx = cost * p.x() - sint * p.y();
1686   double ry = sint * p.x() + cost * p.y();
1687   return SGVec2d(rx, ry);
1688 }
1689
1690 SGGeod MapWidget::unproject(const SGVec2d& p) const
1691 {
1692   // unrotate, if necessary
1693   double cost = cos(-_upHeading * SG_DEGREES_TO_RADIANS),
1694     sint = sin(-_upHeading * SG_DEGREES_TO_RADIANS);
1695   SGVec2d ur(cost * p.x() - sint * p.y(),
1696              sint * p.x() + cost * p.y());
1697
1698   
1699
1700     switch (_projection) {
1701     case PROJECTION_SAMSON_FLAMSTEED:
1702     {
1703         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1704         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1705         double lat = unscaled.y() + _projectionCenter.getLatitudeRad();
1706         double lon = (unscaled.x() / cos(lat)) + _projectionCenter.getLongitudeRad();
1707         return SGGeod::fromRad(lon, lat);
1708     }
1709         
1710     case PROJECTION_ORTHO_AZIMUTH:
1711     {
1712         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1713         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1714         
1715         double phi = length(p);
1716         double c = asin(phi);
1717         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1718         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1719         
1720         double lat = asin(cos(c) * sinTheta1 + ((unscaled.y() * sin(c) * cosTheta1) / phi));
1721         double lon = _projectionCenter.getLongitudeRad() +
1722         atan((unscaled.x()* sin(c)) / (phi  * cosTheta1 * cos(c) - unscaled.y() * sinTheta1 * sin(c)));
1723         return SGGeod::fromRad(lon, lat);
1724     }
1725         
1726     case PROJECTION_SPHERICAL:
1727     {
1728         SGVec2d unscaled = ur * (1.0 / currentScale());
1729         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1730         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1731         SGVec3d cartPt = orient.rotate(SGVec3d(unscaled.x(), unscaled.y(), 0.0));
1732         return SGGeod::fromCart(cartPt + cartCenter);
1733     }
1734     } // of projection mode switch
1735 }
1736
1737 double MapWidget::currentScale() const
1738 {
1739   return 1.0 / pow(2.0, _cachedZoom);
1740 }
1741
1742 void MapWidget::circleAt(const SGVec2d& center, int nSides, double r)
1743 {
1744   glBegin(GL_LINE_LOOP);
1745   double advance = (SGD_PI * 2) / nSides;
1746   glVertex2d(center.x() +r, center.y());
1747   double t=advance;
1748   for (int i=1; i<nSides; ++i) {
1749     glVertex2d(center.x() + (cos(t) * r), center.y() + (sin(t) * r));
1750     t += advance;
1751   }
1752   glEnd();
1753 }
1754
1755 void MapWidget::squareAt(const SGVec2d& center, double r)
1756 {
1757   glBegin(GL_LINE_LOOP);
1758   glVertex2d(center.x() + r, center.y() + r);
1759   glVertex2d(center.x() + r, center.y() - r);
1760   glVertex2d(center.x() - r, center.y() - r);
1761   glVertex2d(center.x() - r, center.y() + r);
1762   glEnd();
1763 }
1764
1765 void MapWidget::circleAtAlt(const SGVec2d& center, int nSides, double r, double r2)
1766 {
1767   glBegin(GL_LINE_LOOP);
1768   double advance = (SGD_PI * 2) / nSides;
1769   glVertex2d(center.x(), center.y() + r);
1770   double t=advance;
1771   for (int i=1; i<nSides; ++i) {
1772     double rr = (i%2 == 0) ? r : r2;
1773     glVertex2d(center.x() + (sin(t) * rr), center.y() + (cos(t) * rr));
1774     t += advance;
1775   }
1776   glEnd();
1777 }
1778
1779 void MapWidget::drawLine(const SGVec2d& p1, const SGVec2d& p2)
1780 {
1781   glBegin(GL_LINES);
1782     glVertex2dv(p1.data());
1783     glVertex2dv(p2.data());
1784   glEnd();
1785 }
1786
1787 void MapWidget::drawLegendBox(const SGVec2d& pos, const std::string& t)
1788 {
1789         std::vector<std::string> lines(simgear::strutils::split(t, "\n"));
1790         const int LINE_LEADING = 4;
1791         const int MARGIN = 4;
1792
1793 // measure
1794         int maxWidth = -1, totalHeight = 0;
1795         int lineHeight = legendFont.getStringHeight();
1796
1797         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1798                 totalHeight += lineHeight;
1799                 if (ln > 0) {
1800                         totalHeight += LINE_LEADING;
1801                 }
1802
1803                 int lw = legendFont.getStringWidth(lines[ln].c_str());
1804                 maxWidth = std::max(maxWidth, lw);
1805         } // of line measurement
1806
1807         if (maxWidth < 0) {
1808                 return; // all lines are empty, don't draw
1809         }
1810
1811         totalHeight += MARGIN * 2;
1812
1813 // draw box
1814         puBox box;
1815         box.min[0] = 0;
1816         box.min[1] = -totalHeight;
1817         box.max[0] = maxWidth + (MARGIN * 2);
1818         box.max[1] = 0;
1819         int border = 1;
1820         box.draw (pos.x(), pos.y(), PUSTYLE_DROPSHADOW, colour, FALSE, border);
1821
1822 // draw lines
1823         int xPos = pos.x() + MARGIN;
1824         int yPos = pos.y() - (lineHeight + MARGIN);
1825         glColor3f(0.8, 0.8, 0.8);
1826
1827         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1828                 legendFont.drawString(lines[ln].c_str(), xPos, yPos);
1829                 yPos -= lineHeight + LINE_LEADING;
1830         }
1831 }
1832
1833 void MapWidget::drawData()
1834 {
1835   std::sort(_dataQueue.begin(), _dataQueue.end(), MapData::order);
1836
1837   int hw = _width >> 1,
1838     hh = _height >> 1;
1839   puBox visBox(makePuBox(-hw, -hh, _width, _height));
1840
1841   unsigned int d = 0;
1842   int drawn = 0;
1843   std::vector<MapData*> drawQueue;
1844
1845   bool drawData = _root->getBoolValue("draw-data");
1846   const int MAX_DRAW_DATA = 25;
1847   const int MAX_DRAW = 50;
1848
1849   for (; (d < _dataQueue.size()) && (drawn < MAX_DRAW); ++d) {
1850     MapData* md = _dataQueue[d];
1851     md->setDataVisible(drawData);
1852
1853     if (md->isClipped(visBox)) {
1854       continue;
1855     }
1856
1857     if (md->overlaps(drawQueue)) {
1858       if (drawData) { // overlapped with data, let's try just the label
1859         md->setDataVisible(false);
1860         if (md->overlaps(drawQueue)) {
1861           continue;
1862         }
1863       } else {
1864         continue;
1865       }
1866     } // of overlaps case
1867
1868     drawQueue.push_back(md);
1869     ++drawn;
1870     if (drawData && (drawn >= MAX_DRAW_DATA)) {
1871       drawData = false;
1872     }
1873   }
1874
1875   // draw lowest-priority first, so higher-priorty items appear on top
1876   std::vector<MapData*>::reverse_iterator r;
1877   for (r = drawQueue.rbegin(); r!= drawQueue.rend(); ++r) {
1878     (*r)->draw();
1879   }
1880
1881   _dataQueue.clear();
1882   KeyDataMap::iterator it = _mapData.begin();
1883   for (; it != _mapData.end(); ) {
1884     it->second->age();
1885     if (it->second->isExpired()) {
1886       delete it->second;
1887       KeyDataMap::iterator cur = it++;
1888       _mapData.erase(cur);
1889     } else {
1890       ++it;
1891     }
1892   } // of expiry iteration
1893 }
1894
1895 bool MapWidget::validDataForKey(void* key)
1896 {
1897   KeyDataMap::iterator it = _mapData.find(key);
1898   if (it == _mapData.end()) {
1899     return false; // no valid data for the key!
1900   }
1901
1902   it->second->resetAge(); // mark data as valid this frame
1903   _dataQueue.push_back(it->second);
1904   return true;
1905 }
1906
1907 void MapWidget::setAnchorForKey(void* key, const SGVec2d& anchor)
1908 {
1909   KeyDataMap::iterator it = _mapData.find(key);
1910   if (it == _mapData.end()) {
1911     throw sg_exception("no valid data for key!");
1912   }
1913
1914   it->second->setAnchor(anchor);
1915 }
1916
1917 MapData* MapWidget::getOrCreateDataForKey(void* key)
1918 {
1919   KeyDataMap::iterator it = _mapData.find(key);
1920   if (it == _mapData.end()) {
1921     return createDataForKey(key);
1922   }
1923
1924   it->second->resetAge(); // mark data as valid this frame
1925   _dataQueue.push_back(it->second);
1926   return it->second;
1927 }
1928
1929 MapData* MapWidget::createDataForKey(void* key)
1930 {
1931   KeyDataMap::iterator it = _mapData.find(key);
1932   if (it != _mapData.end()) {
1933     throw sg_exception("duplicate data requested for key!");
1934   }
1935
1936   MapData* d =  new MapData(0);
1937   _mapData[key] = d;
1938   _dataQueue.push_back(d);
1939   d->resetAge();
1940   return d;
1941 }
1942
1943 void MapWidget::clearData()
1944 {
1945   KeyDataMap::iterator it = _mapData.begin();
1946   for (; it != _mapData.end(); ++it) {
1947     delete it->second;
1948   }
1949   
1950   _mapData.clear();
1951 }
1952
1953 int MapWidget::displayHeading(double h) const
1954 {
1955   if (_magneticHeadings) {
1956     h -= _magVar->get_magvar() * SG_RADIANS_TO_DEGREES;
1957   }
1958   
1959   SG_NORMALIZE_RANGE(h, 0.0, 360.0);
1960   return SGMiscd::roundToInt(h);
1961 }