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