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