Canvas: expose set/clearFocusElement to Nasal.
[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_AZIMUTHAL_EQUIDISTANT;
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     FGFlightHistory* history = (FGFlightHistory*) globals->get_subsystem("history");
658     if (history && _root->getBoolValue("draw-flight-history")) {
659         _flightHistoryPath = history->pathForHistory();
660     } else {
661         _flightHistoryPath.clear();
662     }
663
664 // make spatial queries. This can trigger loading of XML files, etc, so we do
665 // not want to do it in draw(), which can be called from an arbitrary OSG
666 // rendering thread.
667     
668     MapAirportFilter af(_root);
669     if (_cachedZoom <= SHOW_DETAIL2_ZOOM) {
670         // show all airports when zoomed in sufficently
671         af.showAll();
672     }
673     
674     bool partial = false;
675     FGPositionedList newItemsToDraw =
676         FGPositioned::findWithinRangePartial(_projectionCenter, _drawRangeNm, &af, partial);
677     
678     bool fixes = _root->getBoolValue("draw-fixes");
679     NavaidFilter f(fixes, _root->getBoolValue("draw-navaids"));
680     if (f.minType() <= f.maxType()) {
681         FGPositionedList navs = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f);
682         newItemsToDraw.insert(newItemsToDraw.end(), navs.begin(), navs.end());
683     }
684
685     FGPositioned::TypeFilter tf(FGPositioned::COUNTRY);
686     if (_cachedZoom <= SHOW_DETAIL_ZOOM) {
687         tf.addType(FGPositioned::CITY);
688     }
689     
690     if (_cachedZoom <= SHOW_DETAIL2_ZOOM) {
691         tf.addType(FGPositioned::TOWN);
692     }
693     
694     FGPositionedList poi = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &tf);
695     newItemsToDraw.insert(newItemsToDraw.end(), poi.begin(), poi.end());
696     
697     _itemsToDraw.swap(newItemsToDraw);
698     
699     updateAIObjects();
700 }
701
702 void MapWidget::updateAIObjects()
703 {
704     if (!_root->getBoolValue("draw-traffic") || (_cachedZoom > SHOW_DETAIL_ZOOM)) {
705         _aiDrawVec.clear();
706         return;
707     }
708     
709     AIDrawVec newDrawVec;
710     
711     const SGPropertyNode* ai = fgGetNode("/ai/models", true);
712     for (int i = 0; i < ai->nChildren(); ++i) {
713         const SGPropertyNode *model = ai->getChild(i);
714         // skip bad or dead entries
715         if (!model || model->getIntValue("id", -1) == -1) {
716             continue;
717         }
718         
719         SGGeod pos = SGGeod::fromDegFt(
720                                        model->getDoubleValue("position/longitude-deg"),
721                                        model->getDoubleValue("position/latitude-deg"),
722                                        model->getDoubleValue("position/altitude-ft"));
723         
724         double dist = SGGeodesy::distanceNm(_projectionCenter, pos);
725         if (dist > _drawRangeNm) {
726             continue;
727         }
728     
729         newDrawVec.push_back(DrawAIObject((SGPropertyNode*) model, pos));
730     } // of ai/models iteration
731
732     _aiDrawVec.swap(newDrawVec);
733 }
734
735 void MapWidget::draw(int dx, int dy)
736 {
737   GLint sx = (int) abox.min[0],
738     sy = (int) abox.min[1];
739   glScissor(dx + sx, dy + sy, _width, _height);
740   glEnable(GL_SCISSOR_TEST);
741
742   glMatrixMode(GL_MODELVIEW);
743   glPushMatrix();
744   // center drawing about the widget center (which is also the
745   // projection centre)
746   glTranslated(dx + sx + (_width/2), dy + sy + (_height/2), 0.0);
747
748   drawLatLonGrid();
749
750   if (_aircraftUp) {
751     int textHeight = legendFont.getStringHeight() + 5;
752
753     // draw heading line
754     SGVec2d loc = project(_aircraft);
755     glColor3f(1.0, 1.0, 1.0);
756     drawLine(loc, SGVec2d(loc.x(), (_height / 2) - textHeight));
757
758     double y = (_height / 2) - textHeight;
759     char buf[16];
760     ::snprintf(buf, 16, "%d", _displayHeading);
761     int sw = legendFont.getStringWidth(buf);
762     legendFont.drawString(buf, loc.x() - sw/2, y);
763   }
764
765   drawPositioned();
766   drawTraffic();
767   drawGPSData();
768   drawNavRadio(fgGetNode("/instrumentation/nav[0]", false));
769   drawNavRadio(fgGetNode("/instrumentation/nav[1]", false));
770   paintAircraftLocation(_aircraft);
771   drawFlightHistory();
772   paintRoute();
773   paintRuler();
774
775   drawData();
776
777   glPopMatrix();
778   glDisable(GL_SCISSOR_TEST);
779 }
780
781 void MapWidget::paintRuler()
782 {
783   if (_clickGeod == SGGeod()) {
784     return;
785   }
786
787   SGVec2d acftPos = project(_aircraft);
788   SGVec2d clickPos = project(_clickGeod);
789
790   glColor4f(0.0, 1.0, 1.0, 0.6);
791   drawLine(acftPos, clickPos);
792
793   circleAtAlt(clickPos, 8, 10, 5);
794
795   double dist, az, az2;
796   SGGeodesy::inverse(_aircraft, _clickGeod, az, az2, dist);
797   char buffer[1024];
798         ::snprintf(buffer, 1024, "%03d/%.1fnm",
799                 displayHeading(az), dist * SG_METER_TO_NM);
800
801   MapData* d = getOrCreateDataForKey((void*) RULER_LEGEND_KEY);
802   d->setLabel(buffer);
803   d->setAnchor(clickPos);
804   d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
805   d->setPriority(20000);
806 }
807
808 void MapWidget::paintAircraftLocation(const SGGeod& aircraftPos)
809 {
810   SGVec2d loc = project(aircraftPos);
811
812   double hdg = fgGetDouble("/orientation/heading-deg");
813
814   glLineWidth(2.0);
815   glColor4f(1.0, 1.0, 0.0, 1.0);
816   glPushMatrix();
817   glTranslated(loc.x(), loc.y(), 0.0);
818   glRotatef(hdg - _upHeading, 0.0, 0.0, -1.0);
819
820   const SGVec2d wingspan(12, 0);
821   const SGVec2d nose(0, 8);
822   const SGVec2d tail(0, -14);
823   const SGVec2d tailspan(4,0);
824
825   drawLine(-wingspan, wingspan);
826   drawLine(nose, tail);
827   drawLine(tail - tailspan, tail + tailspan);
828
829   glPopMatrix();
830   glLineWidth(1.0);
831 }
832
833 void MapWidget::paintRoute()
834 {
835   if (_route->numWaypts() < 2) {
836     return;
837   }
838
839   RoutePath path(_route->flightPlan());
840
841 // first pass, draw the actual lines
842   glLineWidth(2.0);
843
844   for (int w=0; w<_route->numWaypts(); ++w) {
845     SGGeodVec gv(path.pathForIndex(w));
846     if (gv.empty()) {
847       continue;
848     }
849
850     if (w < _route->currentIndex()) {
851       glColor4f(0.5, 0.5, 0.5, 0.7);
852     } else {
853       glColor4f(1.0, 0.0, 1.0, 1.0);
854     }
855
856     flightgear::WayptRef wpt(_route->wayptAtIndex(w));
857     if (wpt->flag(flightgear::WPT_MISS)) {
858       glEnable(GL_LINE_STIPPLE);
859       glLineStipple(1, 0x00FF);
860     }
861
862     glBegin(GL_LINE_STRIP);
863     for (unsigned int i=0; i<gv.size(); ++i) {
864       SGVec2d p = project(gv[i]);
865       glVertex2d(p.x(), p.y());
866     }
867
868     glEnd();
869     glDisable(GL_LINE_STIPPLE);
870   }
871
872   glLineWidth(1.0);
873 // second pass, draw waypoint symbols and data
874   for (int w=0; w < _route->numWaypts(); ++w) {
875     flightgear::WayptRef wpt(_route->wayptAtIndex(w));
876     SGGeod g = path.positionForIndex(w);
877     if (g == SGGeod()) {
878       continue; // Vectors or similar
879     }
880
881     SGVec2d p = project(g);
882     glColor4f(1.0, 0.0, 1.0, 1.0);
883     circleAtAlt(p, 8, 12, 5);
884
885     std::ostringstream legend;
886     legend << wpt->ident();
887     if (wpt->altitudeRestriction() != flightgear::RESTRICT_NONE) {
888       legend << '\n' << SGMiscd::roundToInt(wpt->altitudeFt()) << '\'';
889     }
890
891     if (wpt->speedRestriction() == flightgear::SPEED_RESTRICT_MACH) {
892       legend << '\n' << wpt->speedMach() << "M";
893     } else if (wpt->speedRestriction() != flightgear::RESTRICT_NONE) {
894       legend << '\n' << SGMiscd::roundToInt(wpt->speedKts()) << "Kts";
895     }
896
897     MapData* d = getOrCreateDataForKey(reinterpret_cast<void*>(w * 2));
898     d->setText(legend.str());
899     d->setLabel(wpt->ident());
900     d->setAnchor(p);
901     d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
902     d->setPriority(w < _route->currentIndex() ? 9000 : 12000);
903
904   } // of second waypoint iteration
905 }
906
907 void MapWidget::drawFlightHistory()
908 {
909   if (_flightHistoryPath.empty())
910     return;
911     
912   // first pass, draw the actual lines
913   glLineWidth(2.0);
914   
915   glColor4f(0.0, 0.0, 1.0, 0.7);
916
917   glBegin(GL_LINE_STRIP);
918   for (unsigned int i=0; i<_flightHistoryPath.size(); ++i) {
919     SGVec2d p = project(_flightHistoryPath[i]);
920     glVertex2d(p.x(), p.y());
921   }
922   
923   glEnd();
924 }
925
926 /**
927  * Round a SGGeod to an arbitrary precision.
928  * For example, passing precision of 0.5 will round to the nearest 0.5 of
929  * a degree in both lat and lon - passing in 3.0 rounds to the nearest 3 degree
930  * multiple, and so on.
931  */
932 static SGGeod roundGeod(double precision, const SGGeod& g)
933 {
934   double lon = SGMiscd::round(g.getLongitudeDeg() / precision);
935   double lat = SGMiscd::round(g.getLatitudeDeg() / precision);
936
937   return SGGeod::fromDeg(lon * precision, lat * precision);
938 }
939
940 bool MapWidget::drawLineClipped(const SGVec2d& a, const SGVec2d& b)
941 {
942   double minX = SGMiscd::min(a.x(), b.x()),
943     minY = SGMiscd::min(a.y(), b.y()),
944     maxX = SGMiscd::max(a.x(), b.x()),
945     maxY = SGMiscd::max(a.y(), b.y());
946
947   int hh = _height >> 1, hw = _width >> 1;
948
949   if ((maxX < -hw) || (minX > hw) || (minY > hh) || (maxY < -hh)) {
950     return false;
951   }
952
953   glVertex2dv(a.data());
954   glVertex2dv(b.data());
955   return true;
956 }
957
958 SGVec2d MapWidget::gridPoint(int ix, int iy)
959 {
960         int key = (ix + 0x7fff) | ((iy + 0x7fff) << 16);
961         GridPointCache::iterator it = _gridCache.find(key);
962         if (it != _gridCache.end()) {
963                 return it->second;
964         }
965
966         SGGeod gp = SGGeod::fromDeg(
967     _gridCenter.getLongitudeDeg() + ix * _gridSpacing,
968                 _gridCenter.getLatitudeDeg() + iy * _gridSpacing);
969
970         SGVec2d proj = project(gp);
971         _gridCache[key] = proj;
972         return proj;
973 }
974
975 void MapWidget::drawLatLonGrid()
976 {
977   // Larger grid spacing when zoomed out, to prevent clutter
978   if (_cachedZoom < SHOW_DETAIL_ZOOM) {
979     _gridSpacing = 1.0;
980   } else {
981     _gridSpacing = 5.0;
982   }
983   
984   _gridCenter = roundGeod(_gridSpacing, _projectionCenter);
985   _gridCache.clear();
986
987   int ix = 0;
988   int iy = 0;
989
990   glColor4f(0.8, 0.8, 0.8, 0.4);
991   glBegin(GL_LINES);
992   bool didDraw;
993   do {
994     didDraw = false;
995     ++ix;
996     ++iy;
997
998     for (int x = -ix; x < ix; ++x) {
999       didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x+1, -iy));
1000       didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x+1, iy));
1001       didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x, -iy + 1));
1002       didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x, iy - 1));
1003     }
1004
1005     for (int y = -iy; y < iy; ++y) {
1006       didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix, y+1));
1007       didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix + 1, y));
1008       didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix, y+1));
1009       didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix - 1, y));
1010     }
1011
1012     if (ix > (90/_gridSpacing)-1) {
1013       break;
1014     }
1015   } while (didDraw);
1016
1017   glEnd();
1018 }
1019
1020 void MapWidget::drawGPSData()
1021 {
1022   std::string gpsMode = _gps->getStringValue("mode");
1023
1024   SGGeod wp0Geod = SGGeod::fromDeg(
1025         _gps->getDoubleValue("wp/wp[0]/longitude-deg"),
1026         _gps->getDoubleValue("wp/wp[0]/latitude-deg"));
1027
1028   SGGeod wp1Geod = SGGeod::fromDeg(
1029         _gps->getDoubleValue("wp/wp[1]/longitude-deg"),
1030         _gps->getDoubleValue("wp/wp[1]/latitude-deg"));
1031
1032 // draw track line
1033   double gpsTrackDeg = _gps->getDoubleValue("indicated-track-true-deg");
1034   double gpsSpeed = _gps->getDoubleValue("indicated-ground-speed-kt");
1035   double az2;
1036
1037   if (gpsSpeed > 3.0) { // only draw track line if valid
1038     SGGeod trackRadial;
1039     SGGeodesy::direct(_aircraft, gpsTrackDeg, _drawRangeNm * SG_NM_TO_METER, trackRadial, az2);
1040
1041     glColor4f(1.0, 1.0, 0.0, 1.0);
1042     glEnable(GL_LINE_STIPPLE);
1043     glLineStipple(1, 0x00FF);
1044     drawLine(project(_aircraft), project(trackRadial));
1045     glDisable(GL_LINE_STIPPLE);
1046   }
1047
1048   if (gpsMode == "dto") {
1049     SGVec2d wp0Pos = project(wp0Geod);
1050     SGVec2d wp1Pos = project(wp1Geod);
1051
1052     glColor4f(1.0, 0.0, 1.0, 1.0);
1053     drawLine(wp0Pos, wp1Pos);
1054
1055   }
1056
1057   if (_gps->getBoolValue("scratch/valid")) {
1058     // draw scratch data
1059
1060   }
1061 }
1062
1063 void MapWidget::drawPositioned()
1064 {
1065   for (unsigned int i=0; i<_itemsToDraw.size(); ++i) {
1066       FGPositionedRef p = _itemsToDraw[i];
1067       switch (p->type()) {
1068           case FGPositioned::AIRPORT:
1069               drawAirport((FGAirport*) p.get());
1070               break;
1071           case FGPositioned::NDB:
1072               drawNDB(false, (FGNavRecord*) p.get());
1073               break;
1074           case FGPositioned::VOR:
1075               drawVOR(false, (FGNavRecord*) p.get());
1076               break;
1077           case FGPositioned::FIX:
1078               drawFix((FGFix*) p.get());
1079               break;
1080          case FGPositioned::TOWN:
1081           case FGPositioned::CITY:
1082           case FGPositioned::COUNTRY:
1083               drawPOI(p);
1084               break;
1085               
1086           default:
1087               SG_LOG(SG_GENERAL, SG_WARN, "unhandled type in MapWidget::drawPositioned");
1088       } // of positioned type switch
1089   } // of items to draw iteration
1090 }
1091
1092 void MapWidget::drawNDB(bool tuned, FGNavRecord* ndb)
1093 {
1094   SGVec2d pos = project(ndb->geod());
1095
1096   if (tuned) {
1097     glColor3f(0.0, 1.0, 1.0);
1098   } else {
1099     glColor3f(0.0, 0.0, 0.0);
1100   }
1101
1102   glEnable(GL_LINE_STIPPLE);
1103   glLineStipple(1, 0x00FF);
1104   circleAt(pos, 20, 6);
1105   circleAt(pos, 20, 10);
1106   glDisable(GL_LINE_STIPPLE);
1107
1108   if (validDataForKey(ndb)) {
1109     setAnchorForKey(ndb, pos);
1110     return;
1111   }
1112
1113   char buffer[1024];
1114         ::snprintf(buffer, 1024, "%s\n%s %3.0fKhz",
1115                 ndb->name().c_str(), ndb->ident().c_str(),ndb->get_freq()/100.0);
1116
1117   MapData* d = createDataForKey(ndb);
1118   d->setPriority(40);
1119   d->setLabel(ndb->ident());
1120   d->setText(buffer);
1121   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1122   d->setAnchor(pos);
1123
1124 }
1125
1126 void MapWidget::drawVOR(bool tuned, FGNavRecord* vor)
1127 {
1128   SGVec2d pos = project(vor->geod());
1129   if (tuned) {
1130     glColor3f(0.0, 1.0, 1.0);
1131   } else {
1132     glColor3f(0.0, 0.0, 1.0);
1133   }
1134
1135   circleAt(pos, 6, 9);
1136   circleAt(pos, 8, 1);
1137
1138   if (vor->hasDME())
1139   squareAt(pos, 9);
1140
1141   if (validDataForKey(vor)) {
1142     setAnchorForKey(vor, pos);
1143     return;
1144   }
1145
1146   char buffer[1024];
1147         ::snprintf(buffer, 1024, "%s\n%s %6.3fMhz",
1148                 vor->name().c_str(), vor->ident().c_str(),
1149     vor->get_freq() / 100.0);
1150
1151   MapData* d = createDataForKey(vor);
1152   d->setText(buffer);
1153   d->setLabel(vor->ident());
1154   d->setPriority(tuned ? 10000 : 100);
1155   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1156   d->setAnchor(pos);
1157 }
1158
1159 void MapWidget::drawFix(FGFix* fix)
1160 {
1161   SGVec2d pos = project(fix->geod());
1162   glColor3f(0.0, 0.0, 0.0);
1163   circleAt(pos, 3, 6);
1164
1165   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1166     return; // hide fix labels beyond a certain zoom level
1167   }
1168
1169   if (validDataForKey(fix)) {
1170     setAnchorForKey(fix, pos);
1171     return;
1172   }
1173
1174   MapData* d = createDataForKey(fix);
1175   d->setLabel(fix->ident());
1176   d->setPriority(20);
1177   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1178   d->setAnchor(pos);
1179 }
1180
1181 void MapWidget::drawNavRadio(SGPropertyNode_ptr radio)
1182 {
1183   if (!radio || radio->getBoolValue("slaved-to-gps", false)
1184         || !radio->getBoolValue("in-range", false)) {
1185     return;
1186   }
1187
1188   if (radio->getBoolValue("nav-loc", false)) {
1189     drawTunedLocalizer(radio);
1190   }
1191
1192   // identify the tuned station - unfortunately we don't get lat/lon directly,
1193   // need to do the frequency search again
1194   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1195
1196   FGNavRecord* nav = FGNavList::findByFreq(mhz, _aircraft,
1197                                            FGNavList::navFilter());
1198   if (!nav || (nav->ident() != radio->getStringValue("nav-id"))) {
1199     // mismatch between navradio selection logic and ours!
1200     return;
1201   }
1202
1203   glLineWidth(1.0);
1204   drawVOR(true, nav);
1205
1206   SGVec2d pos = project(nav->geod());
1207   SGGeod range;
1208   double az2;
1209   double trueRadial = radio->getDoubleValue("radials/target-radial-deg");
1210   SGGeodesy::direct(nav->geod(), trueRadial, nav->get_range() * SG_NM_TO_METER, range, az2);
1211   SGVec2d prange = project(range);
1212
1213   SGVec2d norm = normalize(prange - pos);
1214   SGVec2d perp(norm.y(), -norm.x());
1215
1216   circleAt(pos, 64, length(prange - pos));
1217   drawLine(pos, prange);
1218
1219 // draw to/from arrows
1220   SGVec2d midPoint = (pos + prange) * 0.5;
1221   if (radio->getBoolValue("from-flag")) {
1222     norm = -norm;
1223     perp = -perp;
1224   }
1225
1226   int sz = 10;
1227   SGVec2d arrowB = midPoint - (norm * sz) + (perp * sz);
1228   SGVec2d arrowC = midPoint - (norm * sz) - (perp * sz);
1229   drawLine(midPoint, arrowB);
1230   drawLine(arrowB, arrowC);
1231   drawLine(arrowC, midPoint);
1232
1233   drawLine(pos, (2 * pos) - prange); // reciprocal radial
1234 }
1235
1236 void MapWidget::drawTunedLocalizer(SGPropertyNode_ptr radio)
1237 {
1238   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1239   FGNavRecord* loc = FGNavList::findByFreq(mhz, _aircraft, FGNavList::locFilter());
1240   if (!loc || (loc->ident() != radio->getStringValue("nav-id"))) {
1241     // mismatch between navradio selection logic and ours!
1242     return;
1243   }
1244
1245   if (loc->runway()) {
1246     drawILS(true, loc->runway());
1247   }
1248 }
1249
1250 void MapWidget::drawPOI(FGPositioned* poi)
1251 {
1252   SGVec2d pos = project(poi->geod());
1253   glColor3f(1.0, 1.0, 0.0);
1254   glLineWidth(1.0);
1255
1256     int radius = 10;
1257     if (poi->type() == FGPositioned::CITY) {
1258         radius = 8;
1259         glColor3f(0.0, 1.0, 0.0);
1260     } else if (poi->type() == FGPositioned::TOWN) {
1261         radius =  5;
1262         glColor3f(0.2, 1.0, 0.0);
1263     }
1264     
1265   circleAt(pos, 4, radius);
1266
1267   if (validDataForKey(poi)) {
1268     setAnchorForKey(poi, pos);
1269     return;
1270   }
1271
1272   char buffer[1024];
1273         ::snprintf(buffer, 1024, "%s",
1274                 poi->name().c_str());
1275
1276   MapData* d = createDataForKey(poi);
1277   d->setPriority(200);
1278   d->setLabel(poi->ident());
1279   d->setText(buffer);
1280   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1281   d->setAnchor(pos);
1282 }
1283
1284 /*
1285 void MapWidget::drawObstacle(FGPositioned* obs)
1286 {
1287   SGVec2d pos = project(obs->geod());
1288   glColor3f(0.0, 0.0, 0.0);
1289   glLineWidth(2.0);
1290   drawLine(pos, pos + SGVec2d());
1291 }
1292 */
1293
1294 void MapWidget::drawAirport(FGAirport* apt)
1295 {
1296         // draw tower location
1297         SGVec2d towerPos = project(apt->getTowerLocation());
1298
1299   if (_cachedZoom <= SHOW_DETAIL_ZOOM) {
1300     glColor3f(1.0, 1.0, 1.0);
1301     glLineWidth(1.0);
1302
1303     drawLine(towerPos + SGVec2d(3, 0), towerPos + SGVec2d(3, 10));
1304     drawLine(towerPos + SGVec2d(-3, 0), towerPos + SGVec2d(-3, 10));
1305     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(-3, 10));
1306     drawLine(towerPos + SGVec2d(6, 20), towerPos + SGVec2d(3, 10));
1307     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(6, 20));
1308   }
1309
1310   if (validDataForKey(apt)) {
1311     setAnchorForKey(apt, towerPos);
1312   } else {
1313     char buffer[1024];
1314     ::snprintf(buffer, 1024, "%s\n%s",
1315       apt->ident().c_str(), apt->name().c_str());
1316
1317     MapData* d = createDataForKey(apt);
1318     d->setText(buffer);
1319     d->setLabel(apt->ident());
1320     d->setPriority(100 + scoreAirportRunways(apt));
1321     d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 6);
1322     d->setAnchor(towerPos);
1323   }
1324
1325   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1326     return;
1327   }
1328
1329   FGRunwayList runways(apt->getRunwaysWithoutReciprocals());
1330     
1331   for (unsigned int r=0; r<runways.size(); ++r) {
1332     drawRunwayPre(runways[r]);
1333   }
1334
1335   for (unsigned int r=0; r<runways.size(); ++r) {
1336     FGRunway* rwy = runways[r];
1337     drawRunway(rwy);
1338
1339     if (rwy->ILS()) {
1340         drawILS(false, rwy);
1341     }
1342     
1343     if (rwy->reciprocalRunway()) {
1344       FGRunway* recip = rwy->reciprocalRunway();
1345       if (recip->ILS()) {
1346         drawILS(false, recip);
1347       }
1348     }
1349   }
1350
1351   for (unsigned int r=0; r<apt->numHelipads(); ++r) {
1352       FGHelipad* hp = apt->getHelipadByIndex(r);
1353       drawHelipad(hp);
1354   }  // of runway iteration
1355
1356 }
1357
1358 int MapWidget::scoreAirportRunways(FGAirport* apt)
1359 {
1360   bool needHardSurface = _root->getBoolValue("hard-surfaced-airports", true);
1361   double minLength = _root->getDoubleValue("min-runway-length-ft", 2000.0);
1362
1363   FGRunwayList runways(apt->getRunwaysWithoutReciprocals());
1364
1365   int score = 0;
1366   for (unsigned int r=0; r<runways.size(); ++r) {
1367     FGRunway* rwy = runways[r];
1368     if (needHardSurface && !rwy->isHardSurface()) {
1369       continue;
1370     }
1371
1372     if (rwy->lengthFt() < minLength) {
1373       continue;
1374     }
1375
1376     int scoreLength = SGMiscd::roundToInt(rwy->lengthFt() / 200.0);
1377     score += scoreLength;
1378   } // of runways iteration
1379
1380   return score;
1381 }
1382
1383 void MapWidget::drawRunwayPre(FGRunway* rwy)
1384 {
1385   SGVec2d p1 = project(rwy->begin());
1386         SGVec2d p2 = project(rwy->end());
1387
1388   glLineWidth(4.0);
1389   glColor3f(1.0, 0.0, 1.0);
1390         drawLine(p1, p2);
1391 }
1392
1393 void MapWidget::drawRunway(FGRunway* rwy)
1394 {
1395         // line for runway
1396         // optionally show active, stopway, etc
1397         // in legend, show published heading and length
1398         // and threshold elevation
1399
1400   SGVec2d p1 = project(rwy->begin());
1401         SGVec2d p2 = project(rwy->end());
1402   glLineWidth(2.0);
1403   glColor3f(1.0, 1.0, 1.0);
1404   SGVec2d inset = normalize(p2 - p1) * 2;
1405
1406         drawLine(p1 + inset, p2 - inset);
1407
1408   if (validDataForKey(rwy)) {
1409     setAnchorForKey(rwy, (p1 + p2) * 0.5);
1410     return;
1411   }
1412   
1413         char buffer[1024];
1414         ::snprintf(buffer, 1024, "%s/%s\n%03d/%03d\n%.0f'",
1415                 rwy->ident().c_str(),
1416                 rwy->reciprocalRunway()->ident().c_str(),
1417                 displayHeading(rwy->headingDeg()),
1418                 displayHeading(rwy->reciprocalRunway()->headingDeg()),
1419                 rwy->lengthFt());
1420
1421   MapData* d = createDataForKey(rwy);
1422   d->setText(buffer);
1423   d->setLabel(rwy->ident() + "/" + rwy->reciprocalRunway()->ident());
1424   d->setPriority(50);
1425   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1426   d->setAnchor((p1 + p2) * 0.5);
1427 }
1428
1429 void MapWidget::drawILS(bool tuned, FGRunway* rwy)
1430 {
1431         // arrow, tip centered on the landing threshold
1432   // using LOC transmitter position would be more accurate, but
1433   // is visually cluttered
1434         // arrow width is based upon the computed localizer width
1435
1436         FGNavRecord* loc = rwy->ILS();
1437         double halfBeamWidth = loc->localizerWidth() * 0.5;
1438         SGVec2d t = project(rwy->threshold());
1439         SGGeod locEnd;
1440         double rangeM = loc->get_range() * SG_NM_TO_METER;
1441         double radial = loc->get_multiuse();
1442   SG_NORMALIZE_RANGE(radial, 0.0, 360.0);
1443         double az2;
1444
1445 // compute the three end points at the widge end of the arrow
1446         SGGeodesy::direct(loc->geod(), radial, -rangeM, locEnd, az2);
1447         SGVec2d endCentre = project(locEnd);
1448
1449         SGGeodesy::direct(loc->geod(), radial + halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1450         SGVec2d endR = project(locEnd);
1451
1452         SGGeodesy::direct(loc->geod(), radial - halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1453         SGVec2d endL = project(locEnd);
1454
1455 // outline two triangles
1456   glLineWidth(1.0);
1457   if (tuned) {
1458     glColor3f(0.0, 1.0, 1.0);
1459   } else {
1460     glColor3f(0.0, 0.0, 1.0);
1461         }
1462
1463   glBegin(GL_LINE_LOOP);
1464                 glVertex2dv(t.data());
1465                 glVertex2dv(endCentre.data());
1466                 glVertex2dv(endL.data());
1467         glEnd();
1468         glBegin(GL_LINE_LOOP);
1469                 glVertex2dv(t.data());
1470                 glVertex2dv(endCentre.data());
1471                 glVertex2dv(endR.data());
1472         glEnd();
1473
1474         if (validDataForKey(loc)) {
1475     setAnchorForKey(loc, endR);
1476     return;
1477   }
1478
1479         char buffer[1024];
1480         ::snprintf(buffer, 1024, "%s\n%s\n%03d - %3.2fMHz",
1481                 loc->ident().c_str(), loc->name().c_str(),
1482     displayHeading(radial),
1483     loc->get_freq()/100.0);
1484
1485   MapData* d = createDataForKey(loc);
1486   d->setPriority(40);
1487   d->setLabel(loc->ident());
1488   d->setText(buffer);
1489   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1490   d->setAnchor(endR);
1491 }
1492
1493 void MapWidget::drawTraffic()
1494 {
1495     AIDrawVec::const_iterator it;
1496     for (it = _aiDrawVec.begin(); it != _aiDrawVec.end(); ++it) {
1497         drawAI(*it);
1498     }
1499 }
1500
1501 void MapWidget::drawHelipad(FGHelipad* hp)
1502 {
1503   SGVec2d pos = project(hp->geod());
1504   glLineWidth(1.0);
1505   glColor3f(1.0, 0.0, 1.0);
1506   circleAt(pos, 16, 5.0);
1507
1508   if (validDataForKey(hp)) {
1509     setAnchorForKey(hp, pos);
1510     return;
1511   }
1512
1513   char buffer[1024];
1514   ::snprintf(buffer, 1024, "%s\n%03d\n%.0f'",
1515              hp->ident().c_str(),
1516              displayHeading(hp->headingDeg()),
1517              hp->lengthFt());
1518
1519   MapData* d = createDataForKey(hp);
1520   d->setText(buffer);
1521   d->setLabel(hp->ident());
1522   d->setPriority(40);
1523   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 8);
1524   d->setAnchor(pos);
1525 }
1526
1527 void MapWidget::drawAI(const DrawAIObject& dai)
1528 {
1529   SGVec2d p = project(dai.pos);
1530
1531     if (dai.boat) {
1532         glColor3f(0.0, 0.0, 0.5);
1533
1534     } else {
1535         glColor3f(0.0, 0.0, 0.0);
1536     }
1537   glLineWidth(2.0);
1538   circleAt(p, 4, 6.0); // black diamond
1539
1540 // draw heading vector
1541   if (dai.speedKts > 1) {
1542     glLineWidth(1.0);
1543     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1544     double distanceM = dai.speedKts * SG_NM_TO_METER * dt;
1545     SGGeod advance = SGGeodesy::direct(dai.pos, dai.heading, distanceM);
1546     drawLine(p, project(advance));
1547   }
1548    
1549     MapData* d = getOrCreateDataForKey((void*) dai.model);
1550     d->setText(dai.legend);
1551     d->setLabel(dai.label);
1552     d->setPriority(dai.speedKts > 5 ? 60 : 10); // low priority for parked aircraft
1553     d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1554     d->setAnchor(p);
1555 }
1556
1557 SGVec2d MapWidget::project(const SGGeod& geod) const
1558 {
1559   SGVec2d p;
1560   double r = earth_radius_lat(geod.getLatitudeRad());
1561   
1562     switch (_projection) {
1563     case PROJECTION_SAMSON_FLAMSTEED:
1564     {
1565         // Sanson-Flamsteed projection, relative to the projection center
1566         double lonDiff = geod.getLongitudeRad() - _projectionCenter.getLongitudeRad(),
1567         latDiff = geod.getLatitudeRad() - _projectionCenter.getLatitudeRad();
1568         
1569         p = SGVec2d(cos(geod.getLatitudeRad()) * lonDiff, latDiff) * r * currentScale();
1570         break;
1571     }
1572         
1573     case PROJECTION_AZIMUTHAL_EQUIDISTANT:
1574     {
1575         // Azimuthal Equidistant projection, relative to the projection center
1576       // http://www.globmaritime.com/martech/marine-navigation/general-concepts/626-azimuthal-equidistant-projection
1577         double ref_lat = _projectionCenter.getLatitudeRad(),
1578                ref_lon = _projectionCenter.getLongitudeRad(),
1579                lat = geod.getLatitudeRad(),
1580                lon = geod.getLongitudeRad(),
1581                lonDiff = lon - ref_lon;
1582       
1583         double c = acos( sin(ref_lat) * sin(lat) + cos(ref_lat) * cos(lat) * cos(lonDiff) );
1584         if (c == 0.0){
1585             // angular distance from center is 0
1586             p= SGVec2d(0.0, 0.0);
1587             break;
1588         }
1589       
1590         double k = c / sin(c);
1591         double x, y;
1592         if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
1593         {
1594             x = (SGD_PI / 2 - lat) * sin(lonDiff);
1595             y = -(SGD_PI / 2 - lat) * cos(lonDiff);
1596         }
1597         else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
1598         {
1599             x = (SGD_PI / 2 + lat) * sin(lonDiff);
1600             y = (SGD_PI / 2 + lat) * cos(lonDiff);
1601         }
1602         else
1603         {
1604             x = k * cos(lat) * sin(lonDiff);
1605             y = k * ( cos(ref_lat) * sin(lat) - sin(ref_lat) * cos(lat) * cos(lonDiff) );
1606         }
1607         p = SGVec2d(x, y) * r * currentScale();
1608       
1609         break;
1610     }
1611     
1612     case PROJECTION_ORTHO_AZIMUTH:
1613     {
1614         // http://mathworld.wolfram.com/OrthographicProjection.html
1615         double cosTheta = cos(geod.getLatitudeRad());
1616         double sinDLambda = sin(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1617         double cosDLambda = cos(geod.getLongitudeRad() - _projectionCenter.getLongitudeRad());
1618         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1619         double sinTheta = sin(geod.getLatitudeRad());
1620         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1621         
1622         p = SGVec2d(cosTheta * sinDLambda,
1623                     (cosTheta1 * sinTheta) - (sinTheta1 * cosTheta * cosDLambda)) * r * currentScale();
1624         break;
1625     }
1626             
1627     case PROJECTION_SPHERICAL:
1628     {
1629         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1630         SGVec3d cartPt = SGVec3d::fromGeod(geod) - cartCenter;
1631         
1632         // rotate relative to projection center
1633         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1634         cartPt = orient.rotateBack(cartPt);
1635         return SGVec2d(cartPt.y(), cartPt.x()) * currentScale();
1636         break;
1637     }
1638     } // of projection mode switch
1639     
1640   
1641 // rotate as necessary
1642   double cost = cos(_upHeading * SG_DEGREES_TO_RADIANS),
1643     sint = sin(_upHeading * SG_DEGREES_TO_RADIANS);
1644   double rx = cost * p.x() - sint * p.y();
1645   double ry = sint * p.x() + cost * p.y();
1646   return SGVec2d(rx, ry);
1647 }
1648
1649 SGGeod MapWidget::unproject(const SGVec2d& p) const
1650 {
1651   // unrotate, if necessary
1652   double cost = cos(-_upHeading * SG_DEGREES_TO_RADIANS),
1653     sint = sin(-_upHeading * SG_DEGREES_TO_RADIANS);
1654   SGVec2d ur(cost * p.x() - sint * p.y(),
1655              sint * p.x() + cost * p.y());
1656
1657   
1658
1659     switch (_projection) {
1660     case PROJECTION_SAMSON_FLAMSTEED:
1661     {
1662         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1663         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1664         double lat = unscaled.y() + _projectionCenter.getLatitudeRad();
1665         double lon = (unscaled.x() / cos(lat)) + _projectionCenter.getLongitudeRad();
1666         return SGGeod::fromRad(lon, lat);
1667     }
1668         
1669     case PROJECTION_AZIMUTHAL_EQUIDISTANT:
1670     {
1671         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1672         SGVec2d unscaled = ur * (1.0 / currentScale());
1673         double lat = 0,
1674                lon = 0,
1675                ref_lat = _projectionCenter.getLatitudeRad(),
1676                ref_lon = _projectionCenter.getLongitudeRad(),
1677                rho = sqrt(unscaled.x() * unscaled.x() + unscaled.y() * unscaled.y()),
1678                c = rho/r;
1679         
1680         if (rho == 0)
1681         {
1682             lat = ref_lat;
1683             lon = ref_lon;
1684         } 
1685         else
1686         {
1687             lat = asin( cos(c) * sin(ref_lat) + (unscaled.y()  * sin(c) * cos(ref_lat)) / rho);
1688
1689             if (ref_lat == (90 * SG_DEGREES_TO_RADIANS))
1690             {
1691                 lon = ref_lon + atan(-unscaled.x()/unscaled.y());
1692             }
1693             else if (ref_lat == -(90 * SG_DEGREES_TO_RADIANS))
1694             {
1695                 lon = ref_lon + atan(unscaled.x()/unscaled.y());
1696             }
1697             else
1698             {
1699                 lon = ref_lon + atan(unscaled.x() * sin(c) / (rho * cos(ref_lat) * cos(c) - unscaled.y() * sin(ref_lat) * sin(c)));
1700             }
1701          }
1702
1703         return SGGeod::fromRad(lon, lat);
1704     }
1705         
1706     case PROJECTION_ORTHO_AZIMUTH:
1707     {
1708         double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1709         SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1710         
1711         double phi = length(p);
1712         double c = asin(phi);
1713         double sinTheta1 = sin(_projectionCenter.getLatitudeRad());
1714         double cosTheta1 = cos(_projectionCenter.getLatitudeRad());
1715         
1716         double lat = asin(cos(c) * sinTheta1 + ((unscaled.y() * sin(c) * cosTheta1) / phi));
1717         double lon = _projectionCenter.getLongitudeRad() +
1718         atan((unscaled.x()* sin(c)) / (phi  * cosTheta1 * cos(c) - unscaled.y() * sinTheta1 * sin(c)));
1719         return SGGeod::fromRad(lon, lat);
1720     }
1721         
1722     case PROJECTION_SPHERICAL:
1723     {
1724         SGVec2d unscaled = ur * (1.0 / currentScale());
1725         SGQuatd orient = SGQuatd::fromLonLat(_projectionCenter);
1726         SGVec3d cartCenter = SGVec3d::fromGeod(_projectionCenter);
1727         SGVec3d cartPt = orient.rotate(SGVec3d(unscaled.x(), unscaled.y(), 0.0));
1728         return SGGeod::fromCart(cartPt + cartCenter);
1729     }
1730
1731     default:
1732       throw sg_exception("MapWidget::unproject(): requested unknown projection");
1733     } // of projection mode switch
1734 }
1735
1736 double MapWidget::currentScale() const
1737 {
1738   return 1.0 / pow(2.0, _cachedZoom);
1739 }
1740
1741 void MapWidget::circleAt(const SGVec2d& center, int nSides, double r)
1742 {
1743   glBegin(GL_LINE_LOOP);
1744   double advance = (SGD_PI * 2) / nSides;
1745   glVertex2d(center.x() +r, center.y());
1746   double t=advance;
1747   for (int i=1; i<nSides; ++i) {
1748     glVertex2d(center.x() + (cos(t) * r), center.y() + (sin(t) * r));
1749     t += advance;
1750   }
1751   glEnd();
1752 }
1753
1754 void MapWidget::squareAt(const SGVec2d& center, double r)
1755 {
1756   glBegin(GL_LINE_LOOP);
1757   glVertex2d(center.x() + r, center.y() + r);
1758   glVertex2d(center.x() + r, center.y() - r);
1759   glVertex2d(center.x() - r, center.y() - r);
1760   glVertex2d(center.x() - r, center.y() + r);
1761   glEnd();
1762 }
1763
1764 void MapWidget::circleAtAlt(const SGVec2d& center, int nSides, double r, double r2)
1765 {
1766   glBegin(GL_LINE_LOOP);
1767   double advance = (SGD_PI * 2) / nSides;
1768   glVertex2d(center.x(), center.y() + r);
1769   double t=advance;
1770   for (int i=1; i<nSides; ++i) {
1771     double rr = (i%2 == 0) ? r : r2;
1772     glVertex2d(center.x() + (sin(t) * rr), center.y() + (cos(t) * rr));
1773     t += advance;
1774   }
1775   glEnd();
1776 }
1777
1778 void MapWidget::drawLine(const SGVec2d& p1, const SGVec2d& p2)
1779 {
1780   glBegin(GL_LINES);
1781     glVertex2dv(p1.data());
1782     glVertex2dv(p2.data());
1783   glEnd();
1784 }
1785
1786 void MapWidget::drawLegendBox(const SGVec2d& pos, const std::string& t)
1787 {
1788         std::vector<std::string> lines(simgear::strutils::split(t, "\n"));
1789         const int LINE_LEADING = 4;
1790         const int MARGIN = 4;
1791
1792 // measure
1793         int maxWidth = -1, totalHeight = 0;
1794         int lineHeight = legendFont.getStringHeight();
1795
1796         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1797                 totalHeight += lineHeight;
1798                 if (ln > 0) {
1799                         totalHeight += LINE_LEADING;
1800                 }
1801
1802                 int lw = legendFont.getStringWidth(lines[ln].c_str());
1803                 maxWidth = std::max(maxWidth, lw);
1804         } // of line measurement
1805
1806         if (maxWidth < 0) {
1807                 return; // all lines are empty, don't draw
1808         }
1809
1810         totalHeight += MARGIN * 2;
1811
1812 // draw box
1813         puBox box;
1814         box.min[0] = 0;
1815         box.min[1] = -totalHeight;
1816         box.max[0] = maxWidth + (MARGIN * 2);
1817         box.max[1] = 0;
1818         int border = 1;
1819         box.draw (pos.x(), pos.y(), PUSTYLE_DROPSHADOW, colour, FALSE, border);
1820
1821 // draw lines
1822         int xPos = pos.x() + MARGIN;
1823         int yPos = pos.y() - (lineHeight + MARGIN);
1824         glColor3f(0.8, 0.8, 0.8);
1825
1826         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1827                 legendFont.drawString(lines[ln].c_str(), xPos, yPos);
1828                 yPos -= lineHeight + LINE_LEADING;
1829         }
1830 }
1831
1832 void MapWidget::drawData()
1833 {
1834   std::sort(_dataQueue.begin(), _dataQueue.end(), MapData::order);
1835
1836   int hw = _width >> 1,
1837     hh = _height >> 1;
1838   puBox visBox(makePuBox(-hw, -hh, _width, _height));
1839
1840   unsigned int d = 0;
1841   int drawn = 0;
1842   std::vector<MapData*> drawQueue;
1843
1844   bool drawData = _root->getBoolValue("draw-data");
1845   const int MAX_DRAW_DATA = 25;
1846   const int MAX_DRAW = 50;
1847
1848   for (; (d < _dataQueue.size()) && (drawn < MAX_DRAW); ++d) {
1849     MapData* md = _dataQueue[d];
1850     md->setDataVisible(drawData);
1851
1852     if (md->isClipped(visBox)) {
1853       continue;
1854     }
1855
1856     if (md->overlaps(drawQueue)) {
1857       if (drawData) { // overlapped with data, let's try just the label
1858         md->setDataVisible(false);
1859         if (md->overlaps(drawQueue)) {
1860           continue;
1861         }
1862       } else {
1863         continue;
1864       }
1865     } // of overlaps case
1866
1867     drawQueue.push_back(md);
1868     ++drawn;
1869     if (drawData && (drawn >= MAX_DRAW_DATA)) {
1870       drawData = false;
1871     }
1872   }
1873
1874   // draw lowest-priority first, so higher-priorty items appear on top
1875   std::vector<MapData*>::reverse_iterator r;
1876   for (r = drawQueue.rbegin(); r!= drawQueue.rend(); ++r) {
1877     (*r)->draw();
1878   }
1879
1880   _dataQueue.clear();
1881   KeyDataMap::iterator it = _mapData.begin();
1882   for (; it != _mapData.end(); ) {
1883     it->second->age();
1884     if (it->second->isExpired()) {
1885       delete it->second;
1886       KeyDataMap::iterator cur = it++;
1887       _mapData.erase(cur);
1888     } else {
1889       ++it;
1890     }
1891   } // of expiry iteration
1892 }
1893
1894 bool MapWidget::validDataForKey(void* key)
1895 {
1896   KeyDataMap::iterator it = _mapData.find(key);
1897   if (it == _mapData.end()) {
1898     return false; // no valid data for the key!
1899   }
1900
1901   it->second->resetAge(); // mark data as valid this frame
1902   _dataQueue.push_back(it->second);
1903   return true;
1904 }
1905
1906 void MapWidget::setAnchorForKey(void* key, const SGVec2d& anchor)
1907 {
1908   KeyDataMap::iterator it = _mapData.find(key);
1909   if (it == _mapData.end()) {
1910     throw sg_exception("no valid data for key!");
1911   }
1912
1913   it->second->setAnchor(anchor);
1914 }
1915
1916 MapData* MapWidget::getOrCreateDataForKey(void* key)
1917 {
1918   KeyDataMap::iterator it = _mapData.find(key);
1919   if (it == _mapData.end()) {
1920     return createDataForKey(key);
1921   }
1922
1923   it->second->resetAge(); // mark data as valid this frame
1924   _dataQueue.push_back(it->second);
1925   return it->second;
1926 }
1927
1928 MapData* MapWidget::createDataForKey(void* key)
1929 {
1930   KeyDataMap::iterator it = _mapData.find(key);
1931   if (it != _mapData.end()) {
1932     throw sg_exception("duplicate data requested for key!");
1933   }
1934
1935   MapData* d =  new MapData(0);
1936   _mapData[key] = d;
1937   _dataQueue.push_back(d);
1938   d->resetAge();
1939   return d;
1940 }
1941
1942 void MapWidget::clearData()
1943 {
1944   KeyDataMap::iterator it = _mapData.begin();
1945   for (; it != _mapData.end(); ++it) {
1946     delete it->second;
1947   }
1948   
1949   _mapData.clear();
1950 }
1951
1952 int MapWidget::displayHeading(double h) const
1953 {
1954   if (_magneticHeadings) {
1955     h -= _magVar->get_magvar() * SG_RADIANS_TO_DEGREES;
1956   }
1957   
1958   SG_NORMALIZE_RANGE(h, 0.0, 360.0);
1959   return SGMiscd::roundToInt(h);
1960 }
1961
1962 MapWidget::DrawAIObject::DrawAIObject(SGPropertyNode* m, const SGGeod& g) :
1963     model(m),
1964     boat(false),
1965     pos(g),
1966     speedKts(0)
1967 {
1968     std::string name(model->getNameString());
1969     heading = model->getDoubleValue("orientation/true-heading-deg");
1970     
1971     if ((name == "aircraft") || (name == "multiplayer") ||
1972         (name == "wingman") || (name == "tanker"))
1973     {
1974         speedKts = static_cast<int>(model->getDoubleValue("velocities/true-airspeed-kt"));
1975         label = model->getStringValue("callsign", "<>");
1976     
1977         // try to access the flight-plan of the aircraft. There are several layers
1978         // of potential NULL-ness here, so we have to be defensive at each stage.
1979         std::string originICAO, destinationICAO;
1980         FGAIManager* aiManager = static_cast<FGAIManager*>(globals->get_subsystem("ai-model"));
1981         FGAIBasePtr aircraft = aiManager ? aiManager->getObjectFromProperty(model) : NULL;
1982         if (aircraft) {
1983             FGAIAircraft* p = static_cast<FGAIAircraft*>(aircraft.get());
1984             if (p->GetFlightPlan()) {
1985                 if (p->GetFlightPlan()->departureAirport()) {
1986                     originICAO = p->GetFlightPlan()->departureAirport()->ident();
1987                 }
1988                 
1989                 if (p->GetFlightPlan()->arrivalAirport()) {
1990                     destinationICAO = p->GetFlightPlan()->arrivalAirport()->ident();
1991                 }
1992             } // of flight-plan exists
1993         } // of check for AIBase-derived instance
1994         
1995         // draw callsign / altitude / speed
1996         int altFt50 = static_cast<int>(pos.getElevationFt() / 50.0) * 50;
1997         std::ostringstream ss;
1998         ss << model->getStringValue("callsign", "<>");
1999         if (speedKts > 1) {
2000             ss << "\n" << altFt50 << "' " << speedKts << "kts";
2001         }
2002         
2003         if (!originICAO.empty() || ! destinationICAO.empty()) {
2004             ss << "\n" << originICAO << " -> " << destinationICAO;
2005         }
2006
2007         legend = ss.str();
2008     } else if ((name == "ship") || (name == "carrier") || (name == "escort")) {
2009         boat = true;
2010         speedKts = static_cast<int>(model->getDoubleValue("velocities/speed-kts"));
2011         label = model->getStringValue("name", "<>");
2012         
2013         char buffer[1024];
2014         ::snprintf(buffer, 1024, "%s\n%dkts",
2015                    model->getStringValue("name", "<>"),
2016                    speedKts);
2017         legend = buffer;
2018     }
2019 }
2020