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