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