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