Improve magnetic heading handling in MapWidget - following some testing at NZCH
[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/route/waypoint.hxx>
13 #include <simgear/sg_inlines.h>
14 #include <simgear/misc/strutils.hxx>
15 #include <simgear/magvar/magvar.hxx>
16 #include <simgear/timing/sg_time.hxx> // for magVar julianDate
17 #include <simgear/structure/exception.hxx>
18
19 #include <Main/globals.hxx>
20 #include <Main/fg_props.hxx>
21 #include <Autopilot/route_mgr.hxx>
22 #include <Navaids/positioned.hxx>
23 #include <Navaids/navrecord.hxx>
24 #include <Navaids/navlist.hxx>
25 #include <Navaids/fix.hxx>
26 #include <Airports/simple.hxx>
27 #include <Airports/runways.hxx>
28 #include <Main/fg_os.hxx>      // fgGetKeyModifiers()
29 #include <Navaids/routePath.hxx>
30
31 const char* RULER_LEGEND_KEY = "ruler-legend";
32
33 /* equatorial and polar earth radius */
34 const float rec  = 6378137;          // earth radius, equator (?)
35 const float rpol = 6356752.314f;      // earth radius, polar   (?)
36
37 /************************************************************************
38   some trigonometric helper functions
39   (translated more or less directly from Alexei Novikovs perl original)
40 *************************************************************************/
41
42 //Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis)
43 static float earth_radius_lat( float lat )
44 {
45   double a = cos(lat)/rec;
46   double b = sin(lat)/rpol;
47   return 1.0f / sqrt( a * a + b * b );
48 }
49
50 ///////////////////////////////////////////////////////////////////////////
51
52 static puBox makePuBox(int x, int y, int w, int h)
53 {
54   puBox r;
55   r.min[0] = x;
56   r.min[1] = y;
57   r.max[0] =  x + w;
58   r.max[1] = y + h;
59   return r;
60 }
61
62 static bool puBoxIntersect(const puBox& a, const puBox& b)
63 {
64   int x0 = SG_MAX2(a.min[0], b.min[0]);
65   int y0 = SG_MAX2(a.min[1], b.min[1]);
66   int x1 = SG_MIN2(a.max[0], b.max[0]);
67   int y1 = SG_MIN2(a.max[1], b.max[1]);
68
69   return (x0 <= x1) && (y0 <= y1);
70 }
71
72 class MapData;
73 typedef std::vector<MapData*> MapDataVec;
74
75 class MapData
76 {
77 public:
78   static const int HALIGN_LEFT = 1;
79   static const int HALIGN_CENTER = 2;
80   static const int HALIGN_RIGHT = 3;
81
82   static const int VALIGN_TOP = 1 << 4;
83   static const int VALIGN_CENTER = 2 << 4;
84   static const int VALIGN_BOTTOM = 3 << 4;
85
86   MapData(int priority) :
87     _dirtyText(true),
88     _age(0),
89     _priority(priority),
90     _width(0),
91     _height(0),
92     _offsetDir(HALIGN_LEFT | VALIGN_CENTER),
93     _offsetPx(10),
94     _dataVisible(false)
95   {
96   }
97
98   void setLabel(const std::string& label)
99   {
100     if (label == _label) {
101       return; // common case, and saves invalidation
102     }
103
104     _label = label;
105     _dirtyText = true;
106   }
107
108   void setText(const std::string &text)
109   {
110     if (_rawText == text) {
111       return; // common case, and saves invalidation
112     }
113
114     _rawText = text;
115     _dirtyText = true;
116   }
117
118   void setDataVisible(bool vis) {
119     if (vis == _dataVisible) {
120       return;
121     }
122
123     if (_rawText.empty()) {
124       vis = false;
125     }
126
127     _dataVisible = vis;
128     _dirtyText = true;
129   }
130
131   static void setFont(puFont f)
132   {
133     _font = f;
134     _fontHeight = f.getStringHeight();
135     _fontDescender = f.getStringDescender();
136   }
137
138   static void setPalette(puColor* pal)
139   {
140     _palette = pal;
141   }
142
143   void setPriority(int pri)
144   {
145     _priority = pri;
146   }
147
148   int priority() const
149   { return _priority; }
150
151   void setAnchor(const SGVec2d& anchor)
152   {
153     _anchor = anchor;
154   }
155
156   void setOffset(int direction, int px)
157   {
158     if ((_offsetPx == px) && (_offsetDir == direction)) {
159       return;
160     }
161
162     _dirtyOffset = true;
163     _offsetDir = direction;
164     _offsetPx = px;
165   }
166
167   bool isClipped(const puBox& vis) const
168   {
169     validate();
170     if ((_width < 1) || (_height < 1)) {
171       return true;
172     }
173
174     return !puBoxIntersect(vis, box());
175   }
176
177   bool overlaps(const MapDataVec& l) const
178   {
179     validate();
180     puBox b(box());
181
182     MapDataVec::const_iterator it;
183     for (it = l.begin(); it != l.end(); ++it) {
184       if (puBoxIntersect(b, (*it)->box())) {
185         return true;
186       }
187     } // of list iteration
188
189     return false;
190   }
191
192   puBox box() const
193   {
194     validate();
195     return makePuBox(
196       _anchor.x() + _offset.x(),
197       _anchor.y() + _offset.y(),
198       _width, _height);
199   }
200
201   void draw()
202   {
203     validate();
204
205     int xx = _anchor.x() + _offset.x();
206     int yy = _anchor.y() + _offset.y();
207
208     if (_dataVisible) {
209       puBox box(makePuBox(0,0,_width, _height));
210       int border = 1;
211       box.draw(xx, yy, PUSTYLE_DROPSHADOW, _palette, FALSE, border);
212
213       // draw lines
214       int lineHeight = _fontHeight;
215       int xPos = xx + MARGIN;
216       int yPos = yy + _height - (lineHeight + MARGIN);
217       glColor3f(0.8, 0.8, 0.8);
218
219       for (unsigned int ln=0; ln<_lines.size(); ++ln) {
220         _font.drawString(_lines[ln].c_str(), xPos, yPos);
221         yPos -= lineHeight + LINE_LEADING;
222       }
223     } else {
224       glColor3f(0.8, 0.8, 0.8);
225       _font.drawString(_label.c_str(), xx, yy + _fontDescender);
226     }
227   }
228
229   void age()
230   {
231     ++_age;
232   }
233
234   void resetAge()
235   {
236     _age = 0;
237   }
238
239   bool isExpired() const
240   { return (_age > 100); }
241
242   static bool order(MapData* a, MapData* b)
243   {
244     return a->_priority > b->_priority;
245   }
246 private:
247   void validate() const
248   {
249     if (!_dirtyText) {
250       if (_dirtyOffset) {
251         computeOffset();
252       }
253
254       return;
255     }
256
257     if (_dataVisible) {
258       measureData();
259     } else {
260       measureLabel();
261     }
262
263     computeOffset();
264     _dirtyText = false;
265   }
266
267   void measureData() const
268   {
269     _lines = simgear::strutils::split(_rawText, "\n");
270   // measure text to find width and height
271     _width = -1;
272     _height = 0;
273
274     for (unsigned int ln=0; ln<_lines.size(); ++ln) {
275       _height += _fontHeight;
276       if (ln > 0) {
277         _height += LINE_LEADING;
278       }
279
280       int lw = _font.getStringWidth(_lines[ln].c_str());
281       _width = std::max(_width, lw);
282     } // of line measurement
283
284     if ((_width < 1) || (_height < 1)) {
285       // will be clipped
286       return;
287     }
288
289     _height += MARGIN * 2;
290     _width += MARGIN * 2;
291   }
292
293   void measureLabel() const
294   {
295     if (_label.empty()) {
296       _width = _height = -1;
297       return;
298     }
299
300     _height = _fontHeight;
301     _width = _font.getStringWidth(_label.c_str());
302   }
303
304   void computeOffset() const
305   {
306     _dirtyOffset = false;
307     if ((_width <= 0) || (_height <= 0)) {
308       return;
309     }
310
311     int hOffset = 0;
312     int vOffset = 0;
313
314     switch (_offsetDir & 0x0f) {
315     default:
316     case HALIGN_LEFT:
317       hOffset = _offsetPx;
318       break;
319
320     case HALIGN_CENTER:
321       hOffset = -(_width>>1);
322       break;
323
324     case HALIGN_RIGHT:
325       hOffset = -(_offsetPx + _width);
326       break;
327     }
328
329     switch (_offsetDir & 0xf0) {
330     default:
331     case VALIGN_TOP:
332       vOffset = -(_offsetPx + _height);
333       break;
334
335     case VALIGN_CENTER:
336       vOffset = -(_height>>1);
337       break;
338
339     case VALIGN_BOTTOM:
340       vOffset = _offsetPx;
341       break;
342     }
343
344     _offset = SGVec2d(hOffset, vOffset);
345   }
346
347   static const int LINE_LEADING = 3;
348         static const int MARGIN = 3;
349
350   mutable bool _dirtyText;
351   mutable bool _dirtyOffset;
352   int _age;
353   std::string _rawText;
354   std::string _label;
355   mutable std::vector<std::string> _lines;
356   int _priority;
357   mutable int _width, _height;
358   SGVec2d _anchor;
359   int _offsetDir;
360   int _offsetPx;
361   mutable SGVec2d _offset;
362   bool _dataVisible;
363
364   static puFont _font;
365   static puColor* _palette;
366   static int _fontHeight;
367   static int _fontDescender;
368 };
369
370 puFont MapData::_font;
371 puColor* MapData::_palette;
372 int MapData::_fontHeight = 0;
373 int MapData::_fontDescender = 0;
374
375 ///////////////////////////////////////////////////////////////////////////
376
377 const int MAX_ZOOM = 16;
378 const int SHOW_DETAIL_ZOOM = 8;
379 const int CURSOR_PAN_STEP = 32;
380
381 MapWidget::MapWidget(int x, int y, int maxX, int maxY) :
382   puObject(x,y,maxX, maxY)
383 {
384   _route = static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"));
385   _gps = fgGetNode("/instrumentation/gps");
386
387   _width = maxX - x;
388   _height = maxY - y;
389   _hasPanned = false;
390
391   MapData::setFont(legendFont);
392   MapData::setPalette(colour);
393
394   _magVar = new SGMagVar();
395 }
396
397 MapWidget::~MapWidget()
398 {
399   delete _magVar;
400   clearData();
401 }
402
403 void MapWidget::setProperty(SGPropertyNode_ptr prop)
404 {
405   _root = prop;
406   int zoom = _root->getBoolValue("zoom", -1);
407   if (zoom < 0) {
408     _root->setIntValue("zoom", 6); // default zoom
409   }
410   
411   _root->setBoolValue("centre-on-aircraft", true);
412   _root->setBoolValue("draw-data", false);
413   _root->setBoolValue("magnetic-headings", true);
414 }
415
416 void MapWidget::setSize(int w, int h)
417 {
418   puObject::setSize(w, h);
419
420   _width = w;
421   _height = h;
422
423 }
424
425 void MapWidget::doHit( int button, int updown, int x, int y )
426 {
427   puObject::doHit(button, updown, x, y);
428   if (updown == PU_DRAG) {
429     handlePan(x, y);
430     return;
431   }
432
433   if (button == 3) { // mouse-wheel up
434     zoomIn();
435   } else if (button == 4) { // mouse-wheel down
436     zoomOut();
437   }
438
439   if (button != active_mouse_button) {
440     return;
441   }
442
443   _hitLocation = SGVec2d(x - abox.min[0], y - abox.min[1]);
444
445   if (updown == PU_UP) {
446     puDeactivateWidget();
447   } else if (updown == PU_DOWN) {
448     puSetActiveWidget(this, x, y);
449
450     if (fgGetKeyModifiers() & KEYMOD_CTRL) {
451       _clickGeod = unproject(_hitLocation - SGVec2d(_width>>1, _height>>1));
452     }
453   }
454 }
455
456 void MapWidget::handlePan(int x, int y)
457 {
458   SGVec2d delta = SGVec2d(x, y) - _hitLocation;
459   pan(delta);
460   _hitLocation = SGVec2d(x,y);
461 }
462
463 int MapWidget::checkKey (int key, int updown )
464 {
465   if ((updown == PU_UP) || !isVisible () || !isActive () || (window != puGetWindow())) {
466     return FALSE ;
467   }
468
469   switch (key)
470   {
471
472   case PU_KEY_UP:
473     pan(SGVec2d(0, -CURSOR_PAN_STEP));
474     break;
475
476   case PU_KEY_DOWN:
477     pan(SGVec2d(0, CURSOR_PAN_STEP));
478     break ;
479
480   case PU_KEY_LEFT:
481     pan(SGVec2d(CURSOR_PAN_STEP, 0));
482     break;
483
484   case PU_KEY_RIGHT:
485     pan(SGVec2d(-CURSOR_PAN_STEP, 0));
486     break;
487
488   case '-':
489     zoomOut();
490
491     break;
492
493   case '=':
494     zoomIn();
495     break;
496
497   default :
498     return FALSE;
499   }
500
501   return TRUE ;
502 }
503
504 void MapWidget::pan(const SGVec2d& delta)
505 {
506   _hasPanned = true; 
507   _projectionCenter = unproject(-delta);
508 }
509
510 int MapWidget::zoom() const
511 {
512   int z = _root->getIntValue("zoom");
513   SG_CLAMP_RANGE(z, 0, MAX_ZOOM);
514   return z;
515 }
516
517 void MapWidget::zoomIn()
518 {
519   if (zoom() <= 0) {
520     return;
521   }
522
523   _root->setIntValue("zoom", zoom() - 1);
524 }
525
526 void MapWidget::zoomOut()
527 {
528   if (zoom() >= MAX_ZOOM) {
529     return;
530   }
531
532   _root->setIntValue("zoom", zoom() + 1);
533 }
534
535 void MapWidget::draw(int dx, int dy)
536 {
537   _aircraft = SGGeod::fromDeg(fgGetDouble("/position/longitude-deg"),
538     fgGetDouble("/position/latitude-deg"));
539     
540   bool mag = _root->getBoolValue("magnetic-headings");
541   if (mag != _magneticHeadings) {
542     clearData(); // flush cached data text, since it often includes heading
543     _magneticHeadings =  mag;
544   }
545   
546   if (_hasPanned) {
547       _root->setBoolValue("centre-on-aircraft", false);
548       _hasPanned = false;
549   }
550   else if (_root->getBoolValue("centre-on-aircraft")) {
551     _projectionCenter = _aircraft;
552   }
553
554   double julianDate = globals->get_time_params()->getJD();
555   _magVar->update(_projectionCenter, julianDate);
556
557   bool aircraftUp = _root->getBoolValue("aircraft-heading-up");
558   if (aircraftUp) {
559     _upHeading = fgGetDouble("/orientation/heading-deg");
560   } else {
561     _upHeading = 0.0;
562   }
563
564   _cachedZoom = zoom();
565   SGGeod topLeft = unproject(SGVec2d(_width/2, _height/2));
566   // compute draw range, including a fudge factor for ILSs and other 'long'
567   // symbols
568   _drawRangeNm = SGGeodesy::distanceNm(_projectionCenter, topLeft) + 10.0;
569
570 // drawing operations
571   GLint sx = (int) abox.min[0],
572     sy = (int) abox.min[1];
573   glScissor(dx + sx, dy + sy, _width, _height);
574   glEnable(GL_SCISSOR_TEST);
575
576   glMatrixMode(GL_MODELVIEW);
577   glPushMatrix();
578   // cetere drawing about the widget center (which is also the
579   // projection centre)
580   glTranslated(dx + sx + (_width/2), dy + sy + (_height/2), 0.0);
581
582   drawLatLonGrid();
583
584   if (aircraftUp) {
585     int textHeight = legendFont.getStringHeight() + 5;
586
587     // draw heading line
588     SGVec2d loc = project(_aircraft);
589     glColor3f(1.0, 1.0, 1.0);
590     drawLine(loc, SGVec2d(loc.x(), (_height / 2) - textHeight));
591
592     int displayHdg;
593     if (_magneticHeadings) {
594       displayHdg = (int) fgGetDouble("/orientation/heading-magnetic-deg");
595     } else {
596       displayHdg = (int) _upHeading;
597     }
598
599     double y = (_height / 2) - textHeight;
600     char buf[16];
601     ::snprintf(buf, 16, "%d", displayHdg);
602     int sw = legendFont.getStringWidth(buf);
603     legendFont.drawString(buf, loc.x() - sw/2, y);
604   }
605
606   drawAirports();
607   drawNavaids();
608   drawTraffic();
609   drawGPSData();
610   drawNavRadio(fgGetNode("/instrumentation/nav[0]", false));
611   drawNavRadio(fgGetNode("/instrumentation/nav[1]", false));
612   paintAircraftLocation(_aircraft);
613   paintRoute();
614   paintRuler();
615
616   drawData();
617
618   glPopMatrix();
619   glDisable(GL_SCISSOR_TEST);
620 }
621
622 void MapWidget::paintRuler()
623 {
624   if (_clickGeod == SGGeod()) {
625     return;
626   }
627
628   SGVec2d acftPos = project(_aircraft);
629   SGVec2d clickPos = project(_clickGeod);
630
631   glColor4f(0.0, 1.0, 1.0, 0.6);
632   drawLine(acftPos, clickPos);
633
634   circleAtAlt(clickPos, 8, 10, 5);
635
636   double dist, az, az2;
637   SGGeodesy::inverse(_aircraft, _clickGeod, az, az2, dist);
638   char buffer[1024];
639         ::snprintf(buffer, 1024, "%03d/%.1fnm",
640                 displayHeading(az), dist * SG_METER_TO_NM);
641
642   MapData* d = getOrCreateDataForKey((void*) RULER_LEGEND_KEY);
643   d->setLabel(buffer);
644   d->setAnchor(clickPos);
645   d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
646   d->setPriority(20000);
647
648
649 }
650
651 void MapWidget::paintAircraftLocation(const SGGeod& aircraftPos)
652 {
653   SGVec2d loc = project(aircraftPos);
654
655   double hdg = fgGetDouble("/orientation/heading-deg");
656
657   glLineWidth(2.0);
658   glColor4f(1.0, 1.0, 0.0, 1.0);
659   glPushMatrix();
660   glTranslated(loc.x(), loc.y(), 0.0);
661   glRotatef(hdg - _upHeading, 0.0, 0.0, -1.0);
662
663   const SGVec2d wingspan(12, 0);
664   const SGVec2d nose(0, 8);
665   const SGVec2d tail(0, -14);
666   const SGVec2d tailspan(4,0);
667
668   drawLine(-wingspan, wingspan);
669   drawLine(nose, tail);
670   drawLine(tail - tailspan, tail + tailspan);
671
672   glPopMatrix();
673   glLineWidth(1.0);
674 }
675
676 void MapWidget::paintRoute()
677 {
678   if (_route->numWaypts() < 2) {
679     return;
680   }
681
682   RoutePath path(_route->waypts());
683
684 // first pass, draw the actual lines
685   glLineWidth(2.0);
686
687   for (int w=0; w<_route->numWaypts(); ++w) {
688     SGGeodVec gv(path.pathForIndex(w));
689     if (gv.empty()) {
690       continue;
691     }
692
693     if (w < _route->currentIndex()) {
694       glColor4f(0.5, 0.5, 0.5, 0.7);
695     } else {
696       glColor4f(1.0, 0.0, 1.0, 1.0);
697     }
698
699     flightgear::WayptRef wpt(_route->wayptAtIndex(w));
700     if (wpt->flag(flightgear::WPT_MISS)) {
701       glEnable(GL_LINE_STIPPLE);
702       glLineStipple(1, 0x00FF);
703     }
704
705     glBegin(GL_LINE_STRIP);
706     for (unsigned int i=0; i<gv.size(); ++i) {
707       SGVec2d p = project(gv[i]);
708       glVertex2d(p.x(), p.y());
709     }
710
711     glEnd();
712     glDisable(GL_LINE_STIPPLE);
713   }
714
715   glLineWidth(1.0);
716 // second pass, draw waypoint symbols and data
717   for (int w=0; w < _route->numWaypts(); ++w) {
718     flightgear::WayptRef wpt(_route->wayptAtIndex(w));
719     SGGeod g = path.positionForIndex(w);
720     if (g == SGGeod()) {
721       continue; // Vectors or similar
722     }
723
724     SGVec2d p = project(g);
725     glColor4f(1.0, 0.0, 1.0, 1.0);
726     circleAtAlt(p, 8, 12, 5);
727
728     std::ostringstream legend;
729     legend << wpt->ident();
730     if (wpt->altitudeRestriction() != flightgear::RESTRICT_NONE) {
731       legend << '\n' << SGMiscd::roundToInt(wpt->altitudeFt()) << '\'';
732     }
733
734     if (wpt->speedRestriction() == flightgear::SPEED_RESTRICT_MACH) {
735       legend << '\n' << wpt->speedMach() << "M";
736     } else if (wpt->speedRestriction() != flightgear::RESTRICT_NONE) {
737       legend << '\n' << SGMiscd::roundToInt(wpt->speedKts()) << "Kts";
738     }
739
740     MapData* d = getOrCreateDataForKey(reinterpret_cast<void*>(w * 2));
741     d->setText(legend.str());
742     d->setLabel(wpt->ident());
743     d->setAnchor(p);
744     d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15);
745     d->setPriority(w < _route->currentIndex() ? 9000 : 12000);
746
747   } // of second waypoint iteration
748 }
749
750 /**
751  * Round a SGGeod to an arbitrary precision.
752  * For example, passing precision of 0.5 will round to the nearest 0.5 of
753  * a degree in both lat and lon - passing in 3.0 rounds to the nearest 3 degree
754  * multiple, and so on.
755  */
756 static SGGeod roundGeod(double precision, const SGGeod& g)
757 {
758   double lon = SGMiscd::round(g.getLongitudeDeg() / precision);
759   double lat = SGMiscd::round(g.getLatitudeDeg() / precision);
760
761   return SGGeod::fromDeg(lon * precision, lat * precision);
762 }
763
764 bool MapWidget::drawLineClipped(const SGVec2d& a, const SGVec2d& b)
765 {
766   double minX = SGMiscd::min(a.x(), b.x()),
767     minY = SGMiscd::min(a.y(), b.y()),
768     maxX = SGMiscd::max(a.x(), b.x()),
769     maxY = SGMiscd::max(a.y(), b.y());
770
771   int hh = _height >> 1, hw = _width >> 1;
772
773   if ((maxX < -hw) || (minX > hw) || (minY > hh) || (maxY < -hh)) {
774     return false;
775   }
776
777   glVertex2dv(a.data());
778   glVertex2dv(b.data());
779   return true;
780 }
781
782 SGVec2d MapWidget::gridPoint(int ix, int iy)
783 {
784         int key = (ix + 0x7fff) | ((iy + 0x7fff) << 16);
785         GridPointCache::iterator it = _gridCache.find(key);
786         if (it != _gridCache.end()) {
787                 return it->second;
788         }
789
790         SGGeod gp = SGGeod::fromDeg(
791     _gridCenter.getLongitudeDeg() + ix * _gridSpacing,
792                 _gridCenter.getLatitudeDeg() + iy * _gridSpacing);
793
794         SGVec2d proj = project(gp);
795         _gridCache[key] = proj;
796         return proj;
797 }
798
799 void MapWidget::drawLatLonGrid()
800 {
801   _gridSpacing = 1.0;
802   _gridCenter = roundGeod(_gridSpacing, _projectionCenter);
803   _gridCache.clear();
804
805   int ix = 0;
806   int iy = 0;
807
808   glColor4f(0.8, 0.8, 0.8, 0.4);
809   glBegin(GL_LINES);
810   bool didDraw;
811   do {
812     didDraw = false;
813     ++ix;
814     ++iy;
815
816     for (int x = -ix; x < ix; ++x) {
817       didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x+1, -iy));
818       didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x+1, iy));
819       didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x, -iy + 1));
820       didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x, iy - 1));
821
822     }
823
824     for (int y = -iy; y < iy; ++y) {
825       didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix, y+1));
826       didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix + 1, y));
827       didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix, y+1));
828       didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix - 1, y));
829     }
830
831     if (ix > 30) {
832       break;
833     }
834   } while (didDraw);
835
836   glEnd();
837 }
838
839 void MapWidget::drawGPSData()
840 {
841   std::string gpsMode = _gps->getStringValue("mode");
842
843   SGGeod wp0Geod = SGGeod::fromDeg(
844         _gps->getDoubleValue("wp/wp[0]/longitude-deg"),
845         _gps->getDoubleValue("wp/wp[0]/latitude-deg"));
846
847   SGGeod wp1Geod = SGGeod::fromDeg(
848         _gps->getDoubleValue("wp/wp[1]/longitude-deg"),
849         _gps->getDoubleValue("wp/wp[1]/latitude-deg"));
850
851 // draw track line
852   double gpsTrackDeg = _gps->getDoubleValue("indicated-track-true-deg");
853   double gpsSpeed = _gps->getDoubleValue("indicated-ground-speed-kt");
854   double az2;
855
856   if (gpsSpeed > 3.0) { // only draw track line if valid
857     SGGeod trackRadial;
858     SGGeodesy::direct(_aircraft, gpsTrackDeg, _drawRangeNm * SG_NM_TO_METER, trackRadial, az2);
859
860     glColor4f(1.0, 1.0, 0.0, 1.0);
861     glEnable(GL_LINE_STIPPLE);
862     glLineStipple(1, 0x00FF);
863     drawLine(project(_aircraft), project(trackRadial));
864     glDisable(GL_LINE_STIPPLE);
865   }
866
867   if (gpsMode == "dto") {
868     SGVec2d wp0Pos = project(wp0Geod);
869     SGVec2d wp1Pos = project(wp1Geod);
870
871     glColor4f(1.0, 0.0, 1.0, 1.0);
872     drawLine(wp0Pos, wp1Pos);
873
874   }
875
876   if (_gps->getBoolValue("scratch/valid")) {
877     // draw scratch data
878
879   }
880 }
881
882 class MapAirportFilter : public FGAirport::AirportFilter
883 {
884 public:
885   MapAirportFilter(SGPropertyNode_ptr nd)
886   {
887     _heliports = nd->getBoolValue("show-heliports", false);
888     _hardRunwaysOnly = nd->getBoolValue("hard-surfaced-airports", true);
889     _minLengthFt = nd->getDoubleValue("min-runway-length-ft", 2000.0);
890   }
891
892   virtual FGPositioned::Type maxType() const {
893     return _heliports ? FGPositioned::HELIPORT : FGPositioned::AIRPORT;
894   }
895
896   virtual bool passAirport(FGAirport* aApt) const {
897     if (_hardRunwaysOnly) {
898       return aApt->hasHardRunwayOfLengthFt(_minLengthFt);
899     }
900
901     return true;
902   }
903
904 private:
905   bool _heliports;
906   bool _hardRunwaysOnly;
907   double _minLengthFt;
908 };
909
910 void MapWidget::drawAirports()
911 {
912   MapAirportFilter af(_root);
913   FGPositioned::List apts = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &af);
914   for (unsigned int i=0; i<apts.size(); ++i) {
915     drawAirport((FGAirport*) apts[i].get());
916   }
917 }
918
919 class NavaidFilter : public FGPositioned::Filter
920 {
921 public:
922   NavaidFilter(bool fixesEnabled, bool navaidsEnabled) :
923     _fixes(fixesEnabled),
924     _navaids(navaidsEnabled)
925   {}
926
927   virtual bool pass(FGPositioned* aPos) const {
928     if (_fixes && (aPos->type() == FGPositioned::FIX)) {
929       // ignore fixes which end in digits - expirmental
930       if (aPos->ident().length() > 4 && isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) {
931         return false;
932       }
933     }
934
935     return true;
936   }
937
938   virtual FGPositioned::Type minType() const {
939     return _fixes ? FGPositioned::FIX : FGPositioned::VOR;
940   }
941
942   virtual FGPositioned::Type maxType() const {
943     return _navaids ? FGPositioned::NDB : FGPositioned::FIX;
944   }
945
946 private:
947   bool _fixes, _navaids;
948 };
949
950 void MapWidget::drawNavaids()
951 {
952   bool fixes = _root->getBoolValue("draw-fixes");
953   NavaidFilter f(fixes, _root->getBoolValue("draw-navaids"));
954
955   if (f.minType() <= f.maxType()) {
956     FGPositioned::List navs = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f);
957
958     glLineWidth(1.0);
959     for (unsigned int i=0; i<navs.size(); ++i) {
960       FGPositioned::Type ty = navs[i]->type();
961       if (ty == FGPositioned::NDB) {
962         drawNDB(false, (FGNavRecord*) navs[i].get());
963       } else if (ty == FGPositioned::VOR) {
964         drawVOR(false, (FGNavRecord*) navs[i].get());
965       } else if (ty == FGPositioned::FIX) {
966         drawFix((FGFix*) navs[i].get());
967       }
968     } // of navaid iteration
969   } // of navaids || fixes are drawn test
970 }
971
972 void MapWidget::drawNDB(bool tuned, FGNavRecord* ndb)
973 {
974   SGVec2d pos = project(ndb->geod());
975
976   if (tuned) {
977     glColor3f(0.0, 1.0, 1.0);
978   } else {
979     glColor3f(0.0, 0.0, 0.0);
980   }
981
982   glEnable(GL_LINE_STIPPLE);
983   glLineStipple(1, 0x00FF);
984   circleAt(pos, 20, 6);
985   circleAt(pos, 20, 10);
986   glDisable(GL_LINE_STIPPLE);
987
988   if (validDataForKey(ndb)) {
989     setAnchorForKey(ndb, pos);
990     return;
991   }
992
993   char buffer[1024];
994         ::snprintf(buffer, 1024, "%s\n%s %3.0fKhz",
995                 ndb->name().c_str(), ndb->ident().c_str(),ndb->get_freq()/100.0);
996
997   MapData* d = createDataForKey(ndb);
998   d->setPriority(40);
999   d->setLabel(ndb->ident());
1000   d->setText(buffer);
1001   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1002   d->setAnchor(pos);
1003
1004 }
1005
1006 void MapWidget::drawVOR(bool tuned, FGNavRecord* vor)
1007 {
1008   SGVec2d pos = project(vor->geod());
1009   if (tuned) {
1010     glColor3f(0.0, 1.0, 1.0);
1011   } else {
1012     glColor3f(0.0, 0.0, 1.0);
1013   }
1014
1015   circleAt(pos, 6, 8);
1016
1017   if (validDataForKey(vor)) {
1018     setAnchorForKey(vor, pos);
1019     return;
1020   }
1021
1022   char buffer[1024];
1023         ::snprintf(buffer, 1024, "%s\n%s %6.3fMhz",
1024                 vor->name().c_str(), vor->ident().c_str(),
1025     vor->get_freq() / 100.0);
1026
1027   MapData* d = createDataForKey(vor);
1028   d->setText(buffer);
1029   d->setLabel(vor->ident());
1030   d->setPriority(tuned ? 10000 : 100);
1031   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1032   d->setAnchor(pos);
1033 }
1034
1035 void MapWidget::drawFix(FGFix* fix)
1036 {
1037   SGVec2d pos = project(fix->geod());
1038   glColor3f(0.0, 0.0, 0.0);
1039   circleAt(pos, 3, 6);
1040
1041   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1042     return; // hide fix labels beyond a certain zoom level
1043   }
1044
1045   if (validDataForKey(fix)) {
1046     setAnchorForKey(fix, pos);
1047     return;
1048   }
1049
1050   MapData* d = createDataForKey(fix);
1051   d->setLabel(fix->ident());
1052   d->setPriority(20);
1053   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1054   d->setAnchor(pos);
1055 }
1056
1057 void MapWidget::drawNavRadio(SGPropertyNode_ptr radio)
1058 {
1059   if (!radio || radio->getBoolValue("slaved-to-gps", false)
1060         || !radio->getBoolValue("in-range", false)) {
1061     return;
1062   }
1063
1064   if (radio->getBoolValue("nav-loc", false)) {
1065     drawTunedLocalizer(radio);
1066   }
1067
1068   // identify the tuned station - unfortunately we don't get lat/lon directly,
1069   // need to do the frequency search again
1070   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1071   FGNavRecord* nav = globals->get_navlist()->findByFreq(mhz, _aircraft);
1072   if (!nav || (nav->ident() != radio->getStringValue("nav-id"))) {
1073     // mismatch between navradio selection logic and ours!
1074     return;
1075   }
1076
1077   glLineWidth(1.0);
1078   drawVOR(true, nav);
1079
1080   SGVec2d pos = project(nav->geod());
1081   SGGeod range;
1082   double az2;
1083   double trueRadial = radio->getDoubleValue("radials/target-radial-deg");
1084   SGGeodesy::direct(nav->geod(), trueRadial, nav->get_range() * SG_NM_TO_METER, range, az2);
1085   SGVec2d prange = project(range);
1086
1087   SGVec2d norm = normalize(prange - pos);
1088   SGVec2d perp(norm.y(), -norm.x());
1089
1090   circleAt(pos, 64, length(prange - pos));
1091   drawLine(pos, prange);
1092
1093 // draw to/from arrows
1094   SGVec2d midPoint = (pos + prange) * 0.5;
1095   if (radio->getBoolValue("from-flag")) {
1096     norm = -norm;
1097     perp = -perp;
1098   }
1099
1100   int sz = 10;
1101   SGVec2d arrowB = midPoint - (norm * sz) + (perp * sz);
1102   SGVec2d arrowC = midPoint - (norm * sz) - (perp * sz);
1103   drawLine(midPoint, arrowB);
1104   drawLine(arrowB, arrowC);
1105   drawLine(arrowC, midPoint);
1106
1107   drawLine(pos, (2 * pos) - prange); // reciprocal radial
1108 }
1109
1110 void MapWidget::drawTunedLocalizer(SGPropertyNode_ptr radio)
1111 {
1112   double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0);
1113   FGNavRecord* loc = globals->get_loclist()->findByFreq(mhz, _aircraft);
1114   if (!loc || (loc->ident() != radio->getStringValue("nav-id"))) {
1115     // mismatch between navradio selection logic and ours!
1116     return;
1117   }
1118
1119   if (loc->runway()) {
1120     drawILS(true, loc->runway());
1121   }
1122 }
1123
1124 /*
1125 void MapWidget::drawObstacle(FGPositioned* obs)
1126 {
1127   SGVec2d pos = project(obs->geod());
1128   glColor3f(0.0, 0.0, 0.0);
1129   glLineWidth(2.0);
1130   drawLine(pos, pos + SGVec2d());
1131 }
1132 */
1133
1134 void MapWidget::drawAirport(FGAirport* apt)
1135 {
1136         // draw tower location
1137         SGVec2d towerPos = project(apt->getTowerLocation());
1138
1139   if (_cachedZoom <= SHOW_DETAIL_ZOOM) {
1140     glColor3f(1.0, 1.0, 1.0);
1141     glLineWidth(1.0);
1142
1143     drawLine(towerPos + SGVec2d(3, 0), towerPos + SGVec2d(3, 10));
1144     drawLine(towerPos + SGVec2d(-3, 0), towerPos + SGVec2d(-3, 10));
1145     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(-3, 10));
1146     drawLine(towerPos + SGVec2d(6, 20), towerPos + SGVec2d(3, 10));
1147     drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(6, 20));
1148   }
1149
1150   if (validDataForKey(apt)) {
1151     setAnchorForKey(apt, towerPos);
1152   } else {
1153     char buffer[1024];
1154     ::snprintf(buffer, 1024, "%s\n%s",
1155       apt->ident().c_str(), apt->name().c_str());
1156
1157     MapData* d = createDataForKey(apt);
1158     d->setText(buffer);
1159     d->setLabel(apt->ident());
1160     d->setPriority(100 + scoreAirportRunways(apt));
1161     d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 6);
1162     d->setAnchor(towerPos);
1163   }
1164
1165   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1166     return;
1167   }
1168
1169   for (unsigned int r=0; r<apt->numRunways(); ++r) {
1170     FGRunway* rwy = apt->getRunwayByIndex(r);
1171                 if (!rwy->isReciprocal()) {
1172                         drawRunwayPre(rwy);
1173                 }
1174   }
1175
1176         for (unsigned int r=0; r<apt->numRunways(); ++r) {
1177                 FGRunway* rwy = apt->getRunwayByIndex(r);
1178                 if (!rwy->isReciprocal()) {
1179                         drawRunway(rwy);
1180                 }
1181
1182                 if (rwy->ILS()) {
1183                         drawILS(false, rwy);
1184                 }
1185         } // of runway iteration
1186
1187 }
1188
1189 int MapWidget::scoreAirportRunways(FGAirport* apt)
1190 {
1191   bool needHardSurface = _root->getBoolValue("hard-surfaced-airports", true);
1192   double minLength = _root->getDoubleValue("min-runway-length-ft", 2000.0);
1193
1194   int score = 0;
1195   unsigned int numRunways(apt->numRunways());
1196   for (unsigned int r=0; r<numRunways; ++r) {
1197     FGRunway* rwy = apt->getRunwayByIndex(r);
1198     if (rwy->isReciprocal()) {
1199       continue;
1200     }
1201
1202     if (needHardSurface && !rwy->isHardSurface()) {
1203       continue;
1204     }
1205
1206     if (rwy->lengthFt() < minLength) {
1207       continue;
1208     }
1209
1210     int scoreLength = SGMiscd::roundToInt(rwy->lengthFt() / 200.0);
1211     score += scoreLength;
1212   } // of runways iteration
1213
1214   return score;
1215 }
1216
1217 void MapWidget::drawRunwayPre(FGRunway* rwy)
1218 {
1219   SGVec2d p1 = project(rwy->begin());
1220         SGVec2d p2 = project(rwy->end());
1221
1222   glLineWidth(4.0);
1223   glColor3f(1.0, 0.0, 1.0);
1224         drawLine(p1, p2);
1225 }
1226
1227 void MapWidget::drawRunway(FGRunway* rwy)
1228 {
1229         // line for runway
1230         // optionally show active, stopway, etc
1231         // in legend, show published heading and length
1232         // and threshold elevation
1233
1234   SGVec2d p1 = project(rwy->begin());
1235         SGVec2d p2 = project(rwy->end());
1236   glLineWidth(2.0);
1237   glColor3f(1.0, 1.0, 1.0);
1238   SGVec2d inset = normalize(p2 - p1) * 2;
1239
1240         drawLine(p1 + inset, p2 - inset);
1241
1242   if (validDataForKey(rwy)) {
1243     setAnchorForKey(rwy, (p1 + p2) * 0.5);
1244     return;
1245   }
1246   
1247         char buffer[1024];
1248         ::snprintf(buffer, 1024, "%s/%s\n%03d/%03d\n%.0f'",
1249                 rwy->ident().c_str(),
1250                 rwy->reciprocalRunway()->ident().c_str(),
1251                 displayHeading(rwy->headingDeg()),
1252                 displayHeading(rwy->reciprocalRunway()->headingDeg()),
1253                 rwy->lengthFt());
1254
1255   MapData* d = createDataForKey(rwy);
1256   d->setText(buffer);
1257   d->setLabel(rwy->ident() + "/" + rwy->reciprocalRunway()->ident());
1258   d->setPriority(50);
1259   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12);
1260   d->setAnchor((p1 + p2) * 0.5);
1261 }
1262
1263 void MapWidget::drawILS(bool tuned, FGRunway* rwy)
1264 {
1265         // arrow, tip centered on the landing threshold
1266   // using LOC transmitter position would be more accurate, but
1267   // is visually cluttered
1268         // arrow width is based upon the computed localizer width
1269
1270         FGNavRecord* loc = rwy->ILS();
1271         double halfBeamWidth = loc->localizerWidth() * 0.5;
1272         SGVec2d t = project(rwy->threshold());
1273         SGGeod locEnd;
1274         double rangeM = loc->get_range() * SG_NM_TO_METER;
1275         double radial = loc->get_multiuse();
1276   SG_NORMALIZE_RANGE(radial, 0.0, 360.0);
1277         double az2;
1278
1279 // compute the three end points at the widge end of the arrow
1280         SGGeodesy::direct(loc->geod(), radial, -rangeM, locEnd, az2);
1281         SGVec2d endCentre = project(locEnd);
1282
1283         SGGeodesy::direct(loc->geod(), radial + halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1284         SGVec2d endR = project(locEnd);
1285
1286         SGGeodesy::direct(loc->geod(), radial - halfBeamWidth, -rangeM * 1.1, locEnd, az2);
1287         SGVec2d endL = project(locEnd);
1288
1289 // outline two triangles
1290   glLineWidth(1.0);
1291   if (tuned) {
1292     glColor3f(0.0, 1.0, 1.0);
1293   } else {
1294     glColor3f(0.0, 0.0, 1.0);
1295         }
1296
1297   glBegin(GL_LINE_LOOP);
1298                 glVertex2dv(t.data());
1299                 glVertex2dv(endCentre.data());
1300                 glVertex2dv(endL.data());
1301         glEnd();
1302         glBegin(GL_LINE_LOOP);
1303                 glVertex2dv(t.data());
1304                 glVertex2dv(endCentre.data());
1305                 glVertex2dv(endR.data());
1306         glEnd();
1307
1308         if (validDataForKey(loc)) {
1309     setAnchorForKey(loc, endR);
1310     return;
1311   }
1312
1313         char buffer[1024];
1314         ::snprintf(buffer, 1024, "%s\n%s\n%03d - %3.2fMHz",
1315                 loc->ident().c_str(), loc->name().c_str(),
1316     displayHeading(radial),
1317     loc->get_freq()/100.0);
1318
1319   MapData* d = createDataForKey(loc);
1320   d->setPriority(40);
1321   d->setLabel(loc->ident());
1322   d->setText(buffer);
1323   d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10);
1324   d->setAnchor(endR);
1325 }
1326
1327 void MapWidget::drawTraffic()
1328 {
1329   if (!_root->getBoolValue("draw-traffic")) {
1330     return;
1331   }
1332
1333   if (_cachedZoom > SHOW_DETAIL_ZOOM) {
1334     return;
1335   }
1336
1337   const SGPropertyNode* ai = fgGetNode("/ai/models", true);
1338
1339   for (int i = 0; i < ai->nChildren(); ++i) {
1340     const SGPropertyNode *model = ai->getChild(i);
1341     // skip bad or dead entries
1342     if (!model || model->getIntValue("id", -1) == -1) {
1343       continue;
1344     }
1345
1346     const std::string& name(model->getName());
1347     SGGeod pos = SGGeod::fromDegFt(
1348       model->getDoubleValue("position/longitude-deg"),
1349       model->getDoubleValue("position/latitude-deg"),
1350       model->getDoubleValue("position/altitude-ft"));
1351
1352     double dist = SGGeodesy::distanceNm(_projectionCenter, pos);
1353     if (dist > _drawRangeNm) {
1354       continue;
1355     }
1356
1357     double heading = model->getDoubleValue("orientation/true-heading-deg");
1358     if ((name == "aircraft") || (name == "multiplayer") ||
1359         (name == "wingman") || (name == "tanker")) {
1360       drawAIAircraft(model, pos, heading);
1361     } else if ((name == "ship") || (name == "carrier") || (name == "escort")) {
1362       drawAIShip(model, pos, heading);
1363     }
1364   } // of ai/models iteration
1365 }
1366
1367 void MapWidget::drawAIAircraft(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1368 {
1369
1370   SGVec2d p = project(pos);
1371
1372   glColor3f(0.0, 0.0, 0.0);
1373   glLineWidth(2.0);
1374   circleAt(p, 4, 6.0); // black diamond
1375
1376 // draw heading vector
1377   int speedKts = static_cast<int>(model->getDoubleValue("velocities/true-airspeed-kt"));
1378   if (speedKts > 1) {
1379     glLineWidth(1.0);
1380
1381     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1382     double distanceM = speedKts * SG_NM_TO_METER * dt;
1383
1384     SGGeod advance;
1385     double az2;
1386     SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1387
1388     drawLine(p, project(advance));
1389   }
1390
1391   if (validDataForKey((void*) model)) {
1392     setAnchorForKey((void*) model, p);
1393     return;
1394   }
1395
1396   // draw callsign / altitude / speed
1397
1398
1399   char buffer[1024];
1400         ::snprintf(buffer, 1024, "%s\n%d'\n%dkts",
1401                 model->getStringValue("callsign", "<>"),
1402                 static_cast<int>(pos.getElevationFt() / 50.0) * 50,
1403     speedKts);
1404
1405   MapData* d = createDataForKey((void*) model);
1406   d->setText(buffer);
1407   d->setLabel(model->getStringValue("callsign", "<>"));
1408   d->setPriority(speedKts > 5 ? 60 : 10); // low priority for parked aircraft
1409   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1410   d->setAnchor(p);
1411
1412 }
1413
1414 void MapWidget::drawAIShip(const SGPropertyNode* model, const SGGeod& pos, double hdg)
1415 {
1416   SGVec2d p = project(pos);
1417
1418   glColor3f(0.0, 0.0, 0.5);
1419   glLineWidth(2.0);
1420   circleAt(p, 4, 6.0); // blue diamond (to differentiate from aircraft.
1421
1422 // draw heading vector
1423   int speedKts = static_cast<int>(model->getDoubleValue("velocities/speed-kts"));
1424   if (speedKts > 1) {
1425     glLineWidth(1.0);
1426
1427     const double dt = 15.0 / (3600.0); // 15 seconds look-ahead
1428     double distanceM = speedKts * SG_NM_TO_METER * dt;
1429
1430     SGGeod advance;
1431     double az2;
1432     SGGeodesy::direct(pos, hdg, distanceM, advance, az2);
1433
1434     drawLine(p, project(advance));
1435   }
1436
1437   if (validDataForKey((void*) model)) {
1438     setAnchorForKey((void*) model, p);
1439     return;
1440   }
1441
1442   // draw callsign / speed
1443   char buffer[1024];
1444         ::snprintf(buffer, 1024, "%s\n%dkts",
1445                 model->getStringValue("name", "<>"),
1446     speedKts);
1447
1448   MapData* d = createDataForKey((void*) model);
1449   d->setText(buffer);
1450   d->setLabel(model->getStringValue("name", "<>"));
1451   d->setPriority(speedKts > 2 ? 30 : 10); // low priority for slow moving ships
1452   d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10);
1453   d->setAnchor(p);
1454 }
1455
1456 SGVec2d MapWidget::project(const SGGeod& geod) const
1457 {
1458   // Sanson-Flamsteed projection, relative to the projection center
1459   double r = earth_radius_lat(geod.getLatitudeRad());
1460   double lonDiff = geod.getLongitudeRad() - _projectionCenter.getLongitudeRad(),
1461     latDiff = geod.getLatitudeRad() - _projectionCenter.getLatitudeRad();
1462
1463   SGVec2d p = SGVec2d(cos(geod.getLatitudeRad()) * lonDiff, latDiff) * r * currentScale();
1464
1465 // rotate as necessary
1466   double cost = cos(_upHeading * SG_DEGREES_TO_RADIANS),
1467     sint = sin(_upHeading * SG_DEGREES_TO_RADIANS);
1468   double rx = cost * p.x() - sint * p.y();
1469   double ry = sint * p.x() + cost * p.y();
1470   return SGVec2d(rx, ry);
1471 }
1472
1473 SGGeod MapWidget::unproject(const SGVec2d& p) const
1474 {
1475   // unrotate, if necessary
1476   double cost = cos(-_upHeading * SG_DEGREES_TO_RADIANS),
1477     sint = sin(-_upHeading * SG_DEGREES_TO_RADIANS);
1478   SGVec2d ur(cost * p.x() - sint * p.y(),
1479              sint * p.x() + cost * p.y());
1480
1481   double r = earth_radius_lat(_projectionCenter.getLatitudeRad());
1482   SGVec2d unscaled = ur * (1.0 / (currentScale() * r));
1483
1484   double lat = unscaled.y() + _projectionCenter.getLatitudeRad();
1485   double lon = (unscaled.x() / cos(lat)) + _projectionCenter.getLongitudeRad();
1486
1487   return SGGeod::fromRad(lon, lat);
1488 }
1489
1490 double MapWidget::currentScale() const
1491 {
1492   return 1.0 / pow(2.0, _cachedZoom);
1493 }
1494
1495 void MapWidget::circleAt(const SGVec2d& center, int nSides, double r)
1496 {
1497   glBegin(GL_LINE_LOOP);
1498   double advance = (SGD_PI * 2) / nSides;
1499   glVertex2d(center.x(), center.y() + r);
1500   double t=advance;
1501   for (int i=1; i<nSides; ++i) {
1502     glVertex2d(center.x() + (sin(t) * r), center.y() + (cos(t) * r));
1503     t += advance;
1504   }
1505   glEnd();
1506 }
1507
1508 void MapWidget::circleAtAlt(const SGVec2d& center, int nSides, double r, double r2)
1509 {
1510   glBegin(GL_LINE_LOOP);
1511   double advance = (SGD_PI * 2) / nSides;
1512   glVertex2d(center.x(), center.y() + r);
1513   double t=advance;
1514   for (int i=1; i<nSides; ++i) {
1515     double rr = (i%2 == 0) ? r : r2;
1516     glVertex2d(center.x() + (sin(t) * rr), center.y() + (cos(t) * rr));
1517     t += advance;
1518   }
1519   glEnd();
1520 }
1521
1522 void MapWidget::drawLine(const SGVec2d& p1, const SGVec2d& p2)
1523 {
1524   glBegin(GL_LINES);
1525     glVertex2dv(p1.data());
1526     glVertex2dv(p2.data());
1527   glEnd();
1528 }
1529
1530 void MapWidget::drawLegendBox(const SGVec2d& pos, const std::string& t)
1531 {
1532         std::vector<std::string> lines(simgear::strutils::split(t, "\n"));
1533         const int LINE_LEADING = 4;
1534         const int MARGIN = 4;
1535
1536 // measure
1537         int maxWidth = -1, totalHeight = 0;
1538         int lineHeight = legendFont.getStringHeight();
1539
1540         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1541                 totalHeight += lineHeight;
1542                 if (ln > 0) {
1543                         totalHeight += LINE_LEADING;
1544                 }
1545
1546                 int lw = legendFont.getStringWidth(lines[ln].c_str());
1547                 maxWidth = std::max(maxWidth, lw);
1548         } // of line measurement
1549
1550         if (maxWidth < 0) {
1551                 return; // all lines are empty, don't draw
1552         }
1553
1554         totalHeight += MARGIN * 2;
1555
1556 // draw box
1557         puBox box;
1558         box.min[0] = 0;
1559         box.min[1] = -totalHeight;
1560         box.max[0] = maxWidth + (MARGIN * 2);
1561         box.max[1] = 0;
1562         int border = 1;
1563         box.draw (pos.x(), pos.y(), PUSTYLE_DROPSHADOW, colour, FALSE, border);
1564
1565 // draw lines
1566         int xPos = pos.x() + MARGIN;
1567         int yPos = pos.y() - (lineHeight + MARGIN);
1568         glColor3f(0.8, 0.8, 0.8);
1569
1570         for (unsigned int ln=0; ln<lines.size(); ++ln) {
1571                 legendFont.drawString(lines[ln].c_str(), xPos, yPos);
1572                 yPos -= lineHeight + LINE_LEADING;
1573         }
1574 }
1575
1576 void MapWidget::drawData()
1577 {
1578   std::sort(_dataQueue.begin(), _dataQueue.end(), MapData::order);
1579
1580   int hw = _width >> 1,
1581     hh = _height >> 1;
1582   puBox visBox(makePuBox(-hw, -hh, _width, _height));
1583
1584   unsigned int d = 0;
1585   int drawn = 0;
1586   std::vector<MapData*> drawQueue;
1587
1588   bool drawData = _root->getBoolValue("draw-data");
1589   const int MAX_DRAW_DATA = 25;
1590   const int MAX_DRAW = 50;
1591
1592   for (; (d < _dataQueue.size()) && (drawn < MAX_DRAW); ++d) {
1593     MapData* md = _dataQueue[d];
1594     md->setDataVisible(drawData);
1595
1596     if (md->isClipped(visBox)) {
1597       continue;
1598     }
1599
1600     if (md->overlaps(drawQueue)) {
1601       if (drawData) { // overlapped with data, let's try just the label
1602         md->setDataVisible(false);
1603         if (md->overlaps(drawQueue)) {
1604           continue;
1605         }
1606       } else {
1607         continue;
1608       }
1609     } // of overlaps case
1610
1611     drawQueue.push_back(md);
1612     ++drawn;
1613     if (drawData && (drawn >= MAX_DRAW_DATA)) {
1614       drawData = false;
1615     }
1616   }
1617
1618   // draw lowest-priority first, so higher-priorty items appear on top
1619   std::vector<MapData*>::reverse_iterator r;
1620   for (r = drawQueue.rbegin(); r!= drawQueue.rend(); ++r) {
1621     (*r)->draw();
1622   }
1623
1624   _dataQueue.clear();
1625   KeyDataMap::iterator it = _mapData.begin();
1626   for (; it != _mapData.end(); ) {
1627     it->second->age();
1628     if (it->second->isExpired()) {
1629       delete it->second;
1630       KeyDataMap::iterator cur = it++;
1631       _mapData.erase(cur);
1632     } else {
1633       ++it;
1634     }
1635   } // of expiry iteration
1636 }
1637
1638 bool MapWidget::validDataForKey(void* key)
1639 {
1640   KeyDataMap::iterator it = _mapData.find(key);
1641   if (it == _mapData.end()) {
1642     return false; // no valid data for the key!
1643   }
1644
1645   it->second->resetAge(); // mark data as valid this frame
1646   _dataQueue.push_back(it->second);
1647   return true;
1648 }
1649
1650 void MapWidget::setAnchorForKey(void* key, const SGVec2d& anchor)
1651 {
1652   KeyDataMap::iterator it = _mapData.find(key);
1653   if (it == _mapData.end()) {
1654     throw sg_exception("no valid data for key!");
1655   }
1656
1657   it->second->setAnchor(anchor);
1658 }
1659
1660 MapData* MapWidget::getOrCreateDataForKey(void* key)
1661 {
1662   KeyDataMap::iterator it = _mapData.find(key);
1663   if (it == _mapData.end()) {
1664     return createDataForKey(key);
1665   }
1666
1667   it->second->resetAge(); // mark data as valid this frame
1668   _dataQueue.push_back(it->second);
1669   return it->second;
1670 }
1671
1672 MapData* MapWidget::createDataForKey(void* key)
1673 {
1674   KeyDataMap::iterator it = _mapData.find(key);
1675   if (it != _mapData.end()) {
1676     throw sg_exception("duplicate data requested for key!");
1677   }
1678
1679   MapData* d =  new MapData(0);
1680   _mapData[key] = d;
1681   _dataQueue.push_back(d);
1682   d->resetAge();
1683   return d;
1684 }
1685
1686 void MapWidget::clearData()
1687 {
1688   KeyDataMap::iterator it = _mapData.begin();
1689   for (; it != _mapData.end(); ++it) {
1690     delete it->second;
1691   }
1692   
1693   _mapData.clear();
1694 }
1695
1696 int MapWidget::displayHeading(double h) const
1697 {
1698   if (_magneticHeadings) {
1699     h -= _magVar->get_magvar() * SG_RADIANS_TO_DEGREES;
1700   }
1701   
1702   SG_NORMALIZE_RANGE(h, 0.0, 360.0);
1703   return SGMiscd::roundToInt(h);
1704 }