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