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