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