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