Bug 1217, crash on AI traffic data.
[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             if (p->GetFlightPlan()->departureAirport()) {
1576                 originICAO = p->GetFlightPlan()->departureAirport()->ident();
1577             }
1578             
1579             if (p->GetFlightPlan()->arrivalAirport()) {
1580                 destinationICAO = p->GetFlightPlan()->arrivalAirport()->ident();
1581             }
1582         } // of flight-plan exists
1583     } // of check for AIBase-derived instance
1584
1585   // draw callsign / altitude / speed
1586     int altFt50 = static_cast<int>(pos.getElevationFt() / 50.0) * 50;
1587     std::ostringstream ss;
1588     ss << model->getStringValue("callsign", "<>");
1589     if (speedKts > 1) {
1590         ss << "\n" << altFt50 << "' " << speedKts << "kts";
1591     }
1592     
1593     if (!originICAO.empty() || ! destinationICAO.empty()) {
1594         ss << "\n" << originICAO << " -> " << destinationICAO;
1595     }
1596     
1597     MapData* d = getOrCreateDataForKey((void*) model);
1598     d->setText(ss.str().c_str());
1599     d->setLabel(model->getStringValue("callsign", "<>"));
1600     d->setPriority(speedKts > 5 ? 60 : 10); // low priority for parked aircraft
1601     d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1602     d->setAnchor(p);
1603 }
1604
1605 void MapWidget::drawAIShip(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1606 {
1607   SGVec2d p = project(pos);
1608
1609   glColor3f(0.0, 0.0, 0.5);
1610   glLineWidth(2.0);
1611   circleAt(p, 4, 6.0); // blue diamond (to differentiate from aircraft.
1612
1613 // draw heading vector
1614   int speedKts = static_cast<int>(model->getDoubleValue("velocities/speed-kts"));
1615   if (speedKts > 1) {
1616     glLineWidth(1.0);
1617
1618     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1619     double distanceM = speedKts * SG_NM_TO_METER * dt;
1620
1621     SGGeod advance;
1622     double az2;
1623     SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1624
1625     drawLine(p, project(advance));
1626   }
1627
1628   // draw callsign / speed
1629   char buffer[1024];
1630         ::snprintf(buffer, 1024, "%s\n%dkts",
1631                 model->getStringValue("name", "<>"),
1632     speedKts);
1633
1634   MapData* d = getOrCreateDataForKey((void*) model);
1635   d->setText(buffer);
1636   d->setLabel(model->getStringValue("name", "<>"));
1637   d->setPriority(speedKts > 2 ? 30 : 10); // low priority for slow moving ships
1638   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1639   d->setAnchor(p);
1640 }
1641
1642 SGVec2d MapWidget::project(const SGGeod& geod) const
1643 {
1644   SGVec2d p;
1645   double r = earth_radius_lat(geod.getLatitudeRad());
1646   
1647     switch (_projection) {
1648     case PROJECTION_SAMSON_FLAMSTEED:
1649     {
1650         // Sanson-Flamsteed projection, relative to the projection center
1651         double lonDiff = geod.getLongitudeRad() - _projectionCenter.getLongitudeRad(),
1652         latDiff = geod.getLatitudeRad() - _projectionCenter.getLatitudeRad();
1653         
1654         p = SGVec2d(cos(geod.getLatitudeRad()) * lonDiff, latDiff) * r * currentScale();
1655         break;
1656     }
1657             
1658     case PROJECTION_ORTHO_AZIMUTH:
1659     {
1660         // http://mathworld.wolfram.com/OrthographicProjection.html
1661         double cosTheta = cos(geod.getLatitudeRad());
1662         double sinDLambda = sin(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1663         double cosDLambda = cos(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1664         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1665         double sinTheta = sin(geod.getLatitudeRad());
1666         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1667         
1668         p = SGVec2d(cosTheta * sinDLambda,
1669                     (cosTheta1 * sinTheta) - (sinTheta1 * cosTheta * cosDLambda)) * r * currentScale();
1670         break;
1671     }
1672             
1673     case PROJECTION_SPHERICAL:
1674     {
1675         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1676         SGVec3d cartPt = SGVec3d::fromGeod(geod) - cartCenter;
1677         
1678         // rotate relative to projection center
1679         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1680         cartPt = orient.rotateBack(cartPt);
1681         return SGVec2d(cartPt.y(), cartPt.x()) * currentScale();
1682         break;
1683     }
1684     } // of projection mode switch
1685     
1686   
1687 // rotate as necessary
1688   double cost = cos(_upHeading * SG_DEGREES_TO_RADIANS),
1689     sint = sin(_upHeading * SG_DEGREES_TO_RADIANS);
1690   double rx = cost * p.x() - sint * p.y();
1691   double ry = sint * p.x() + cost * p.y();
1692   return SGVec2d(rx, ry);
1693 }
1694
1695 SGGeod MapWidget::unproject(const SGVec2d& p) const
1696 {
1697   // unrotate, if necessary
1698   double cost = cos(-_upHeading * SG_DEGREES_TO_RADIANS),
1699     sint = sin(-_upHeading * SG_DEGREES_TO_RADIANS);
1700   SGVec2d ur(cost * p.x() - sint * p.y(),
1701              sint * p.x() + cost * p.y());
1702
1703   
1704
1705     switch (_projection) {
1706     case PROJECTION_SAMSON_FLAMSTEED:
1707     {
1708         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1709         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1710         double lat = unscaled.y() + _projectionCenter.getLatitudeRad();
1711         double lon = (unscaled.x() / cos(lat)) + _projectionCenter.getLongitudeRad();
1712         return SGGeod::fromRad(lon, lat);
1713     }
1714         
1715     case PROJECTION_ORTHO_AZIMUTH:
1716     {
1717         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1718         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1719         
1720         double phi = length(p);
1721         double c = asin(phi);
1722         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1723         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1724         
1725         double lat = asin(cos(c) * sinTheta1 + ((unscaled.y() * sin(c) * cosTheta1) / phi));
1726         double lon = _projectionCenter.getLongitudeRad() +
1727         atan((unscaled.x()* sin(c)) / (phi  * cosTheta1 * cos(c) - unscaled.y() * sinTheta1 * sin(c)));
1728         return SGGeod::fromRad(lon, lat);
1729     }
1730         
1731     case PROJECTION_SPHERICAL:
1732     {
1733         SGVec2d unscaled = ur * (1.0 / currentScale());
1734         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1735         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1736         SGVec3d cartPt = orient.rotate(SGVec3d(unscaled.x(), unscaled.y(), 0.0));
1737         return SGGeod::fromCart(cartPt + cartCenter);
1738     }
1739     } // of projection mode switch
1740 }
1741
1742 double MapWidget::currentScale() const
1743 {
1744   return 1.0 / pow(2.0, _cachedZoom);
1745 }
1746
1747 void MapWidget::circleAt(const SGVec2d& center, int nSides, double r)
1748 {
1749   glBegin(GL_LINE_LOOP);
1750   double advance = (SGD_PI * 2) / nSides;
1751   glVertex2d(center.x() +r, center.y());
1752   double t=advance;
1753   for (int i=1; i<nSides; ++i) {
1754     glVertex2d(center.x() + (cos(t) * r), center.y() + (sin(t) * r));
1755     t += advance;
1756   }
1757   glEnd();
1758 }
1759
1760 void MapWidget::squareAt(const SGVec2d& center, double r)
1761 {
1762   glBegin(GL_LINE_LOOP);
1763   glVertex2d(center.x() + r, center.y() + r);
1764   glVertex2d(center.x() + r, center.y() - r);
1765   glVertex2d(center.x() - r, center.y() - r);
1766   glVertex2d(center.x() - r, center.y() + r);
1767   glEnd();
1768 }
1769
1770 void MapWidget::circleAtAlt(const SGVec2d& center, int nSides, double r, double r2)
1771 {
1772   glBegin(GL_LINE_LOOP);
1773   double advance = (SGD_PI * 2) / nSides;
1774   glVertex2d(center.x(), center.y() + r);
1775   double t=advance;
1776   for (int i=1; i<nSides; ++i) {
1777     double rr = (i%2 == 0) ? r : r2;
1778     glVertex2d(center.x() + (sin(t) * rr), center.y() + (cos(t) * rr));
1779     t += advance;
1780   }
1781   glEnd();
1782 }
1783
1784 void MapWidget::drawLine(const SGVec2d& p1, const SGVec2d& p2)
1785 {
1786   glBegin(GL_LINES);
1787     glVertex2dv(p1.data());
1788     glVertex2dv(p2.data());
1789   glEnd();
1790 }
1791
1792 void MapWidget::drawLegendBox(const SGVec2d& pos, const std::string& t)
1793 {
1794         std::vector<std::string> lines(simgear::strutils::split(t, "\n"));
1795         const int LINE_LEADING = 4;
1796         const int MARGIN = 4;
1797
1798 // measure
1799         int maxWidth = -1, totalHeight = 0;
1800         int lineHeight = legendFont.getStringHeight();
1801
1802         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1803                 totalHeight += lineHeight;
1804                 if (ln > 0) {
1805                         totalHeight += LINE_LEADING;
1806                 }
1807
1808                 int lw = legendFont.getStringWidth(lines[ln].c_str());
1809                 maxWidth = std::max(maxWidth, lw);
1810         } // of line measurement
1811
1812         if (maxWidth < 0) {
1813                 return; // all lines are empty, don't draw
1814         }
1815
1816         totalHeight += MARGIN * 2;
1817
1818 // draw box
1819         puBox box;
1820         box.min[0] = 0;
1821         box.min[1] = -totalHeight;
1822         box.max[0] = maxWidth + (MARGIN * 2);
1823         box.max[1] = 0;
1824         int border = 1;
1825         box.draw (pos.x(), pos.y(), PUSTYLE_DROPSHADOW, colour, FALSE, border);
1826
1827 // draw lines
1828         int xPos = pos.x() + MARGIN;
1829         int yPos = pos.y() - (lineHeight + MARGIN);
1830         glColor3f(0.8, 0.8, 0.8);
1831
1832         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1833                 legendFont.drawString(lines[ln].c_str(), xPos, yPos);
1834                 yPos -= lineHeight + LINE_LEADING;
1835         }
1836 }
1837
1838 void MapWidget::drawData()
1839 {
1840   std::sort(_dataQueue.begin(), _dataQueue.end(), MapData::order);
1841
1842   int hw = _width >> 1,
1843     hh = _height >> 1;
1844   puBox visBox(makePuBox(-hw, -hh, _width, _height));
1845
1846   unsigned int d = 0;
1847   int drawn = 0;
1848   std::vector<MapData*> drawQueue;
1849
1850   bool drawData = _root->getBoolValue("draw-data");
1851   const int MAX_DRAW_DATA = 25;
1852   const int MAX_DRAW = 50;
1853
1854   for (; (d < _dataQueue.size()) && (drawn < MAX_DRAW); ++d) {
1855     MapData* md = _dataQueue[d];
1856     md->setDataVisible(drawData);
1857
1858     if (md->isClipped(visBox)) {
1859       continue;
1860     }
1861
1862     if (md->overlaps(drawQueue)) {
1863       if (drawData) { // overlapped with data, let's try just the label
1864         md->setDataVisible(false);
1865         if (md->overlaps(drawQueue)) {
1866           continue;
1867         }
1868       } else {
1869         continue;
1870       }
1871     } // of overlaps case
1872
1873     drawQueue.push_back(md);
1874     ++drawn;
1875     if (drawData && (drawn >= MAX_DRAW_DATA)) {
1876       drawData = false;
1877     }
1878   }
1879
1880   // draw lowest-priority first, so higher-priorty items appear on top
1881   std::vector<MapData*>::reverse_iterator r;
1882   for (r = drawQueue.rbegin(); r!= drawQueue.rend(); ++r) {
1883     (*r)->draw();
1884   }
1885
1886   _dataQueue.clear();
1887   KeyDataMap::iterator it = _mapData.begin();
1888   for (; it != _mapData.end(); ) {
1889     it->second->age();
1890     if (it->second->isExpired()) {
1891       delete it->second;
1892       KeyDataMap::iterator cur = it++;
1893       _mapData.erase(cur);
1894     } else {
1895       ++it;
1896     }
1897   } // of expiry iteration
1898 }
1899
1900 bool MapWidget::validDataForKey(void* key)
1901 {
1902   KeyDataMap::iterator it = _mapData.find(key);
1903   if (it == _mapData.end()) {
1904     return false; // no valid data for the key!
1905   }
1906
1907   it->second->resetAge(); // mark data as valid this frame
1908   _dataQueue.push_back(it->second);
1909   return true;
1910 }
1911
1912 void MapWidget::setAnchorForKey(void* key, const SGVec2d& anchor)
1913 {
1914   KeyDataMap::iterator it = _mapData.find(key);
1915   if (it == _mapData.end()) {
1916     throw sg_exception("no valid data for key!");
1917   }
1918
1919   it->second->setAnchor(anchor);
1920 }
1921
1922 MapData* MapWidget::getOrCreateDataForKey(void* key)
1923 {
1924   KeyDataMap::iterator it = _mapData.find(key);
1925   if (it == _mapData.end()) {
1926     return createDataForKey(key);
1927   }
1928
1929   it->second->resetAge(); // mark data as valid this frame
1930   _dataQueue.push_back(it->second);
1931   return it->second;
1932 }
1933
1934 MapData* MapWidget::createDataForKey(void* key)
1935 {
1936   KeyDataMap::iterator it = _mapData.find(key);
1937   if (it != _mapData.end()) {
1938     throw sg_exception("duplicate data requested for key!");
1939   }
1940
1941   MapData* d =  new MapData(0);
1942   _mapData[key] = d;
1943   _dataQueue.push_back(d);
1944   d->resetAge();
1945   return d;
1946 }
1947
1948 void MapWidget::clearData()
1949 {
1950   KeyDataMap::iterator it = _mapData.begin();
1951   for (; it != _mapData.end(); ++it) {
1952     delete it->second;
1953   }
1954   
1955   _mapData.clear();
1956 }
1957
1958 int MapWidget::displayHeading(double h) const
1959 {
1960   if (_magneticHeadings) {
1961     h -= _magVar->get_magvar() * SG_RADIANS_TO_DEGREES;
1962   }
1963   
1964   SG_NORMALIZE_RANGE(h, 0.0, 360.0);
1965   return SGMiscd::roundToInt(h);
1966 }