]> git.mxchange.org Git - flightgear.git/blob - src/Instrumentation/newnavradio.cxx
ADF: code clean-up/documentation
[flightgear.git] / src / Instrumentation / newnavradio.cxx
1 // navradio.cxx -- class to manage a nav radio instance
2 //
3 // Written by Curtis Olson, started April 2000.
4 // Rewritten by Torsten Dreyer, August 2011
5 //
6 // Copyright (C) 2000 - 2011  Curtis L. Olson - http://www.flightgear.org/~curt
7 //
8 // This program is free software; you can redistribute it and/or
9 // modify it under the terms of the GNU General Public License as
10 // published by the Free Software Foundation; either version 2 of the
11 // License, or (at your option) any later version.
12 //
13 // This program is distributed in the hope that it will be useful, but
14 // WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 // General Public License for more details.
17 //
18 // You should have received a copy of the GNU General Public License
19 // along with this program; if not, write to the Free Software
20 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21 //
22
23 #ifdef HAVE_CONFIG_H
24 #  include <config.h>
25 #endif
26
27 #include "newnavradio.hxx"
28
29 #include <assert.h>
30 #include <boost/foreach.hpp>
31
32 #include <simgear/math/interpolater.hxx>
33 #include <simgear/sg_inlines.h>
34 #include <simgear/props/propertyObject.hxx>
35 #include <simgear/misc/strutils.hxx>
36
37 #include <Main/fg_props.hxx>
38 #include <Navaids/navlist.hxx>
39 #include <Sound/audioident.hxx>
40
41 #include "navradio.hxx"
42
43
44 namespace Instrumentation {
45
46 using simgear::PropertyObject;
47
48 /* --------------The Navigation Indicator ----------------------------- */
49
50 class NavIndicator {
51 public:
52     NavIndicator( SGPropertyNode * rootNode ) :
53       _cdi( rootNode->getNode("heading-needle-deflection", true ) ),
54       _cdiNorm( rootNode->getNode("heading-needle-deflection-norm", true ) ),
55       _course( rootNode->getNode("radials/selected-deg", true ) ),
56       _toFlag( rootNode->getNode("to-flag", true ) ),
57       _fromFlag( rootNode->getNode("from-flag", true ) ),
58       _signalQuality( rootNode->getNode("signal-quality-norm", true ) ),
59       _hasGS( rootNode->getNode("has-gs", true ) ),
60       _gsDeflection(rootNode->getNode("gs-needle-deflection", true )),
61       _gsDeflectionDeg(rootNode->getNode("gs-needle-deflection-deg", true )),
62       _gsDeflectionNorm(rootNode->getNode("gs-needle-deflection-norm", true ))
63   {
64   }
65
66   virtual ~NavIndicator() {}
67
68   /**
69    * set the normalized CDI deflection
70    * @param norm the cdi deflection normalized [-1..1]
71    */
72   void setCDI( double norm )
73   {
74       _cdi = norm * 10.0;
75       _cdiNorm = norm;
76   }
77
78   /**
79    * set the normalized GS deflection
80    * @param norm the gs deflection normalized to [-1..1]
81    */
82   void setGS( double norm )
83   {
84       _gsDeflectionNorm = norm;
85       _gsDeflectionDeg = norm * 0.7;
86       _gsDeflection = norm * 3.5;
87   }
88
89   void setGS( bool enabled )
90   {
91       _hasGS = enabled;
92       if( !enabled ) {
93         setGS( 0.0 );
94       }
95   }
96
97   void showFrom( bool on )
98   {
99       _fromFlag = on;
100   }
101
102   void showTo( bool on )
103   {
104       _toFlag = on;
105   }
106       
107   void setSelectedCourse( double course )
108   {
109       _course = course;
110   }
111
112   double getSelectedCourse() const
113   {
114       return SGMiscd::normalizePeriodic(0.0, 360.0, _course );
115   }
116
117   void setSignalQuality( double signalQuality )
118   {
119       _signalQuality = signalQuality;
120   }
121
122 private:
123   PropertyObject<double> _cdi;
124   PropertyObject<double> _cdiNorm;
125   PropertyObject<double> _course;
126   PropertyObject<double> _toFlag;
127   PropertyObject<double> _fromFlag;
128   PropertyObject<double> _signalQuality;
129   PropertyObject<double> _hasGS;
130   PropertyObject<double> _gsDeflection;
131   PropertyObject<double> _gsDeflectionDeg;
132   PropertyObject<double> _gsDeflectionNorm;
133 };
134
135 /* ---------------------------------------------------------------- */
136
137 class NavRadioComponent {
138 public:
139   NavRadioComponent( const std::string & name, SGPropertyNode_ptr rootNode );
140   virtual ~NavRadioComponent();
141
142   virtual void   update( double dt, const SGGeod & aircraftPosition );
143   virtual void   search( double frequency, const SGGeod & aircraftPosition );
144   virtual double getRange_nm( const SGGeod & aircraftPosition );
145   virtual void   display( NavIndicator & navIndicator ) = 0;
146   virtual bool   valid() const { return NULL != _navRecord && true == _serviceable; }
147   virtual const std::string getIdent() const { return _ident; }
148
149 protected:
150   virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
151   virtual FGNavList * getNavaidList() = 0;
152
153   // General-purpose sawtooth function.  Graph looks like this:
154   //         /\                                    .
155   //       \/
156   // Odd symmetry, inversion symmetry about the origin.
157   // Unit slope at the origin.
158   // Max 1, min -1, period 4.
159   // Two zero-crossings per period, one with + slope, one with - slope.
160   // Useful for false localizer courses.
161   static double sawtooth(double xx)
162   {
163     return 4.0 * fabs(xx/4.0 + 0.25 - floor(xx/4.0 + 0.75)) - 1.0;
164   }
165
166   SGPropertyNode_ptr _rootNode;
167   const std::string _name;
168   FGNavRecord * _navRecord;
169   PropertyObject<bool>   _serviceable;
170   PropertyObject<double> _signalQuality_norm;
171   PropertyObject<double> _trueBearingTo_deg;
172   PropertyObject<double> _trueBearingFrom_deg;
173   PropertyObject<double> _trackDistance_m;
174   PropertyObject<double> _slantDistance_m;
175   PropertyObject<double> _heightAboveStation_ft;
176   PropertyObject<string> _ident;
177   PropertyObject<bool>   _inRange;
178   PropertyObject<double> _range_nm;
179 };
180
181 class NavRadioComponentWithIdent : public NavRadioComponent {
182 public:
183   NavRadioComponentWithIdent( const std::string & name, SGPropertyNode_ptr rootNode, AudioIdent * audioIdent );
184   virtual ~NavRadioComponentWithIdent();
185   void update( double dt, const SGGeod & aircraftPosition );
186 protected:
187   static std::string getIdentString( const std::string & name, int index );
188 private:
189   AudioIdent * _audioIdent;
190   PropertyObject<double> _identVolume;
191   PropertyObject<bool>   _identEnabled;
192 };
193
194 std::string NavRadioComponentWithIdent::getIdentString( const std::string & name, int index )
195 {
196   std::ostringstream temp;
197   temp << name << "-ident-" << index;
198   return temp.str();
199 }
200
201 NavRadioComponentWithIdent::NavRadioComponentWithIdent( const std::string & name, SGPropertyNode_ptr rootNode, AudioIdent * audioIdent ) :
202   NavRadioComponent( name, rootNode ),
203   _audioIdent( audioIdent ),
204   _identVolume( rootNode->getNode(name,true)->getNode("ident-volume",true) ),
205   _identEnabled( rootNode->getNode(name,true)->getNode("ident-enabled",true) )
206 {
207   _audioIdent->init();
208
209 }
210 NavRadioComponentWithIdent::~NavRadioComponentWithIdent()
211 {
212   delete _audioIdent;
213 }
214
215 void NavRadioComponentWithIdent::update( double dt, const SGGeod & aircraftPosition )
216 {
217   NavRadioComponent::update( dt, aircraftPosition );
218   _audioIdent->update( dt );
219
220   if( false == ( valid() && _identEnabled && _signalQuality_norm > 0.1 ) ) {
221       _audioIdent->setIdent("", 0.0 );
222       return;
223   }
224   _audioIdent->setIdent( _ident, SGMiscd::clip(_identVolume, 0.0, 1.0) );
225 }
226
227 NavRadioComponent::NavRadioComponent( const std::string & name, SGPropertyNode_ptr rootNode ) :
228   _rootNode(rootNode),
229   _name(name),
230   _navRecord(NULL),
231   _serviceable( rootNode->getNode(name,true)->getNode("serviceable",true) ),
232   _signalQuality_norm( rootNode->getNode(name,true)->getNode("signal-quality-norm",true) ),
233   _trueBearingTo_deg( rootNode->getNode(name,true)->getNode("true-bearing-to-deg",true) ),
234   _trueBearingFrom_deg( rootNode->getNode(name,true)->getNode("true-bearing-from-deg",true) ),
235   _trackDistance_m( rootNode->getNode(name,true)->getNode("track-distance-m",true) ),
236   _slantDistance_m( rootNode->getNode(name,true)->getNode("slant-distance-m",true) ),
237   _heightAboveStation_ft( rootNode->getNode(name,true)->getNode("height-above-station-ft",true) ),
238   _ident( rootNode->getNode(name,true)->getNode("ident",true) ),
239   _inRange( rootNode->getNode(name,true)->getNode("in-range",true) ),
240   _range_nm( rootNode->getNode(_name,true)->getNode("range-nm",true) )
241 {
242   simgear::props::Type typ = _serviceable.node()->getType();
243   if ((typ == simgear::props::NONE) || (typ == simgear::props::UNSPECIFIED))
244     _serviceable = true;
245 }
246
247 NavRadioComponent::~NavRadioComponent()
248 {
249 }
250
251 double NavRadioComponent::getRange_nm( const SGGeod & aircraftPosition )
252
253   if( _navRecord == NULL ) return 0.0; // no station: no range
254   double d = _navRecord->get_range();
255   if( d <= SGLimitsd::min() ) return 25.0; // no configured range: arbitrary number
256   return d; // configured range
257 }
258
259 void NavRadioComponent::search( double frequency, const SGGeod & aircraftPosition )
260 {
261   if( NULL == (_navRecord = getNavaidList()->findByFreq(frequency, aircraftPosition )) ) {
262     SG_LOG(SG_INSTR,SG_ALERT, "No " << _name << " available at " << frequency );
263     _ident = "";
264     return;
265   }
266   SG_LOG(SG_INSTR,SG_ALERT, "Using " << _name << "'" << _navRecord->get_ident() << "' for " << frequency );
267   _ident = _navRecord->ident();
268 }
269
270 double NavRadioComponent::computeSignalQuality_norm( const SGGeod & aircraftPosition )
271 {
272   if( false == valid() ) return 0.0;
273
274   double distance_nm = _slantDistance_m * SG_METER_TO_NM;
275   double range_nm = _range_nm;
276
277   // assume signal quality is 100% up to the published range and 
278   // decay with the distance squared further out
279   if ( distance_nm <= range_nm ) return 1.0;
280   return range_nm*range_nm/(distance_nm*distance_nm);
281 }
282
283 void NavRadioComponent::update( double dt, const SGGeod & aircraftPosition )
284 {
285     if( false == valid() ) {
286       _signalQuality_norm = 0.0;
287       _trueBearingTo_deg = 0.0;
288       _trueBearingFrom_deg = 0.0;
289       _trackDistance_m = 0.0;
290       _slantDistance_m = 0.0;
291       return;
292     } 
293
294     _slantDistance_m = dist(_navRecord->cart(), SGVec3d::fromGeod(aircraftPosition));
295
296     double az1 = 0.0, az2 = 0.0, dist = 0.0;
297     SGGeodesy::inverse(aircraftPosition, _navRecord->geod(), az1, az2, dist );
298     _trueBearingTo_deg = az1; _trueBearingFrom_deg = az2; _trackDistance_m = dist;
299     _heightAboveStation_ft = SGMiscd::max(0.0, aircraftPosition.getElevationFt() - _navRecord->get_elev_ft());
300
301     _range_nm = getRange_nm(aircraftPosition);
302     _signalQuality_norm = computeSignalQuality_norm( aircraftPosition );
303     _inRange = _signalQuality_norm > 0.2;
304 }
305
306 /* ---------------------------------------------------------------- */
307
308 static std::string VORTablePath( const char * name )
309 {
310     SGPath path( globals->get_fg_root() );
311     path.append( "Navaids" );
312     path.append(name);
313     return path.str();
314 }
315
316 class VOR : public NavRadioComponentWithIdent {
317 public:
318   VOR( SGPropertyNode_ptr rootNode);
319   virtual ~VOR();
320   virtual void update( double dt, const SGGeod & aircraftPosition );
321   virtual void display( NavIndicator & navIndicator );
322   virtual double getRange_nm(const SGGeod & aircraftPosition);
323 protected:
324   virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
325   virtual FGNavList * getNavaidList();
326
327 private:
328   double _totalTime;
329   class ServiceVolume {
330   public:
331     ServiceVolume() :
332       term_tbl(VORTablePath("range.term")),
333       low_tbl(VORTablePath("range.low")),
334       high_tbl(VORTablePath("range.high")) {
335     }
336     double adjustRange( double height_ft, double nominalRange_nm );
337
338   private:
339     SGInterpTable term_tbl;
340     SGInterpTable low_tbl;
341     SGInterpTable high_tbl;
342   } _serviceVolume;
343
344   PropertyObject<double> _radial;
345   PropertyObject<double> _radialInbound;
346 };
347
348 // model standard VOR/DME/TACAN service volumes as per AIM 1-1-8
349 double VOR::ServiceVolume::adjustRange( double height_ft, double nominalRange_nm )
350 {
351     if (nominalRange_nm < SGLimitsd::min() )
352       nominalRange_nm = FG_NAV_DEFAULT_RANGE;
353     
354     // extend out actual usable range to be 1.3x the published safe range
355     const double usability_factor = 1.3;
356
357     // assumptions we model the standard service volume, plus
358     // ... rather than specifying a cylinder, we model a cone that
359     // contains the cylinder.  Then we put an upside down cone on top
360     // to model diminishing returns at too-high altitudes.
361
362     if ( nominalRange_nm < 25.0 + SG_EPSILON ) {
363         // Standard Terminal Service Volume
364         return term_tbl.interpolate( height_ft ) * usability_factor;
365     } else if ( nominalRange_nm < 50.0 + SG_EPSILON ) {
366         // Standard Low Altitude Service Volume
367         // table is based on range of 40, scale to actual range
368         return low_tbl.interpolate( height_ft ) * nominalRange_nm / 40.0
369             * usability_factor;
370     } else {
371         // Standard High Altitude Service Volume
372         // table is based on range of 130, scale to actual range
373         return high_tbl.interpolate( height_ft ) * nominalRange_nm / 130.0
374             * usability_factor;
375     }
376 }
377
378 VOR::VOR( SGPropertyNode_ptr rootNode) :
379   NavRadioComponentWithIdent("vor", rootNode, new VORAudioIdent(getIdentString(string("vor"), rootNode->getIndex()))),
380   _totalTime(0.0),
381   _radial( rootNode->getNode(_name,true)->getNode("radial",true) ),
382   _radialInbound( rootNode->getNode(_name,true)->getNode("radial-inbound",true) )
383 {
384 }
385
386 VOR::~VOR()
387 {
388 }
389
390 double VOR::getRange_nm( const SGGeod & aircraftPosition )
391 {
392   return _serviceVolume.adjustRange( _heightAboveStation_ft, _navRecord->get_range() );
393 }
394
395 FGNavList * VOR::getNavaidList()
396 {
397   return globals->get_navlist();
398 }
399
400 double VOR::computeSignalQuality_norm( const SGGeod & aircraftPosition )
401 {
402   // apply cone of confusion. Some sources say it's opening angle is 53deg, others estimate
403   // a diameter of 1NM per 6000ft (approx. 45deg). ICAO Annex 10 says minimum 40deg.
404   // We use 1NM@6000ft and a distance-squared
405   // function to make signal-quality=100% 0.5NM@6000ft from the center and zero overhead
406   double cone_of_confusion_width = 0.5 * _heightAboveStation_ft / 6000.0 * SG_NM_TO_METER;
407   if( _trackDistance_m < cone_of_confusion_width ) {
408     double d = cone_of_confusion_width <= SGLimitsd::min() ? 1 : 
409               (1 - _trackDistance_m/cone_of_confusion_width);
410     return 1-d*d;
411   } 
412
413   // use default decay function outside the cone of confusion
414   return NavRadioComponentWithIdent::computeSignalQuality_norm( aircraftPosition );
415 }
416
417 void VOR::update( double dt, const SGGeod & aircraftPosition )
418 {
419   _totalTime += dt;
420   NavRadioComponentWithIdent::update( dt, aircraftPosition );
421
422   if( false == valid() ) {
423       _radial = 0.0;
424       return;
425   }
426
427   // an arbitrary error function
428   double error = 0.5*(sin(_totalTime/11.0) + sin(_totalTime/23.0));
429
430   // add 1% error at 100% signal-quality
431   // add 50% error at  0% signal-quality
432   // of full deflection (+/-10deg)
433   double e = 10.0 * ( 0.01 + (1-_signalQuality_norm) * 0.49 ) * error;
434
435   // compute magnetic bearing from the station (aka current radial)
436   double r = SGMiscd::normalizePeriodic(0.0, 360.0, _trueBearingFrom_deg - _navRecord->get_multiuse() + e );
437
438   _radial = r;
439   _radialInbound = SGMiscd::normalizePeriodic(0.0,360.0, 180.0 + _radial);
440 }
441
442 void VOR::display( NavIndicator & navIndicator )
443 {
444   if( false == valid() ) return;
445
446   double offset = SGMiscd::normalizePeriodic(-180.0,180.0,_radial - navIndicator.getSelectedCourse());
447   bool to = fabs(offset) >= 90.0;
448
449   if( to ) offset = -offset + copysign(180.0,offset);
450
451   navIndicator.showTo( to );
452   navIndicator.showFrom( !to );
453   // normalize to +/- 1.0 for +/- 10deg, decrease deflection with decreasing signal
454   navIndicator.setCDI( SGMiscd::clip( -offset/10.0, -1.0, 1.0 ) * _signalQuality_norm );
455   navIndicator.setSignalQuality( _signalQuality_norm );
456 }
457
458 /* ---------------------------------------------------------------- */
459 class LOC : public NavRadioComponentWithIdent {
460 public:
461   LOC( SGPropertyNode_ptr rootNode );
462   virtual ~LOC();
463   virtual void update( double dt, const SGGeod & aircraftPosition );
464   virtual void search( double frequency, const SGGeod & aircraftPosition );
465   virtual void display( NavIndicator & navIndicator );
466   virtual double getRange_nm(const SGGeod & aircraftPosition);
467
468 protected:
469   virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
470   virtual FGNavList * getNavaidList();
471
472 private:
473   class ServiceVolume {
474   public:
475       ServiceVolume();
476       double adjustRange( double azimuthAngle_deg, double elevationAngle_deg );
477   private:
478       SGInterpTable _azimuthTable;
479       SGInterpTable _elevationTable;
480   } _serviceVolume;
481   PropertyObject<double> _localizerOffset_norm;
482   PropertyObject<double> _localizerWidth_deg;
483 };
484
485 LOC::ServiceVolume::ServiceVolume()
486 {
487 // maybe this: http://www.tpub.com/content/aviation2/P-1244/P-12440125.htm
488   // ICAO Annex 10 - 3.1.3.2.2: The emission from the localizer
489   // shall be horizontally polarized
490   // very rough abstraction of a 5-element yagi antenna's
491   // E-plane radiation diagram
492   _azimuthTable.addEntry(   0.0, 1.0 );
493   _azimuthTable.addEntry(  10.0, 1.0 );
494   _azimuthTable.addEntry(  30.0, 0.75 );
495   _azimuthTable.addEntry(  40.0, 0.50 );
496   _azimuthTable.addEntry(  50.0, 0.20 );
497   _azimuthTable.addEntry(  60.0, 0.10 );
498   _azimuthTable.addEntry(  70.0, 0.20 );
499   _azimuthTable.addEntry(  80.0, 0.10 );
500   _azimuthTable.addEntry(  90.0, 0.05 );
501   _azimuthTable.addEntry( 105.0, 0.10 );
502   _azimuthTable.addEntry( 130.0, 0.05 );
503   _azimuthTable.addEntry( 150.0, 0.30 );
504   _azimuthTable.addEntry( 160.0, 0.40 );
505   _azimuthTable.addEntry( 170.0, 0.50 );
506   _azimuthTable.addEntry( 180.0, 0.50 );
507
508   _elevationTable.addEntry(   0.0, 0.1 );
509   _elevationTable.addEntry(  1.05, 1.0 );
510   _elevationTable.addEntry(  7.00, 1.0 );
511   _elevationTable.addEntry(  45.0, 0.3 );
512   _elevationTable.addEntry(  90.0, 0.1 );
513   _elevationTable.addEntry( 180.0, 0.01 );
514 }
515
516 double LOC::ServiceVolume::adjustRange( double azimuthAngle_deg, double elevationAngle_deg )
517 {
518     return _azimuthTable.interpolate( fabs(azimuthAngle_deg) ) * 
519         _elevationTable.interpolate( fabs(elevationAngle_deg) );
520 }
521
522 LOC::LOC( SGPropertyNode_ptr rootNode) :
523   NavRadioComponentWithIdent("loc", rootNode, new LOCAudioIdent(getIdentString(string("loc"), rootNode->getIndex()))),
524   _serviceVolume(),
525   _localizerOffset_norm( rootNode->getNode(_name,true)->getNode("offset-norm",true) ),
526   _localizerWidth_deg( rootNode->getNode(_name,true)->getNode("width-deg",true) )
527 {
528 }
529
530 LOC::~LOC()
531 {
532 }
533
534 FGNavList * LOC::getNavaidList()
535 {
536   return globals->get_loclist();
537 }
538
539 void LOC::search( double frequency, const SGGeod & aircraftPosition )
540 {
541   NavRadioComponentWithIdent::search( frequency, aircraftPosition );
542   if( false == valid() ) {
543       _localizerWidth_deg = 0.0;
544       return;
545   }
546
547   // cache slightly expensive value, 
548   // sanitized in FGNavRecord::localizerWidth() to  never become zero
549   _localizerWidth_deg = _navRecord->localizerWidth();
550 }
551
552 /* Localizer coverage (ICAO Annex 10 Volume I 3.1.3.3 
553   25NM within +/-10 deg from the front course line
554   17NM between 10 and 35deg from the front course line
555   10NM outside of +/- 35deg  if coverage is provided
556   at and above a height of 2000ft above threshold or
557   1000ft above the highest point within intermediate
558   and final approach areas. Upper limit is a surface
559   extending outward from the localizer and inclined at
560   7 degrees above the horizontal
561  */
562 double LOC::getRange_nm(const SGGeod & aircraftPosition)
563 {
564   double elevationAngle = ::atan2(_heightAboveStation_ft*SG_FEET_TO_METER, _trackDistance_m)*SG_RADIANS_TO_DEGREES;
565   double azimuthAngle = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - _navRecord->get_multiuse() );
566
567   // looks like our navrecord declared range is based on 10NM?
568   return  _navRecord->get_range() * _serviceVolume.adjustRange( azimuthAngle, elevationAngle );
569 }
570
571 double LOC::computeSignalQuality_norm( const SGGeod & aircraftPosition )
572 {
573   return NavRadioComponentWithIdent::computeSignalQuality_norm( aircraftPosition );
574 }
575
576 void LOC::update( double dt, const SGGeod & aircraftPosition )
577 {
578   NavRadioComponentWithIdent::update( dt, aircraftPosition );
579
580   if( false == valid() ) {
581     _localizerOffset_norm = 0.0;
582     return;
583   }
584
585   double offset = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - _navRecord->get_multiuse() );
586
587   // The factor of 30.0 gives a period of 120 which gives us 3 cycles and six 
588   // zeros i.e. six courses: one front course, one back course, and four 
589   // false courses. Three of the six are reverse sensing.
590   offset = 30.0 * sawtooth(offset / 30.0);
591
592   // normalize offset to the localizer width, scale and clip to [-1..1]
593   offset = SGMiscd::clip( 2.0 * offset / _localizerWidth_deg, -1.0, 1.0 );
594   
595   _localizerOffset_norm = offset;
596 }
597
598 void LOC::display( NavIndicator & navIndicator )
599 {
600   if( false == valid() ) 
601     return;
602
603   navIndicator.showTo( true );
604   navIndicator.showFrom( false );
605
606   navIndicator.setCDI( _localizerOffset_norm * _signalQuality_norm );
607   navIndicator.setSignalQuality( _signalQuality_norm );
608 }
609
610 class GS : public NavRadioComponent {
611 public:
612   GS( SGPropertyNode_ptr rootNode);
613   virtual ~GS();
614   virtual void update( double dt, const SGGeod & aircraftPosition );
615   virtual void search( double frequency, const SGGeod & aircraftPosition );
616   virtual void display( NavIndicator & navIndicator );
617
618   virtual double getRange_nm(const SGGeod & aircraftPosition);
619 protected:
620   virtual FGNavList * getNavaidList();
621
622 private:
623   class ServiceVolume {
624   public:
625       ServiceVolume();
626       double adjustRange( double azimuthAngle_deg, double elevationAngle_deg );
627   private:
628       SGInterpTable _azimuthTable;
629       SGInterpTable _elevationTable;
630   } _serviceVolume;
631   static SGVec3d tangentVector(const SGGeod& midpoint, const double heading);
632
633   PropertyObject<double>  _targetGlideslope_deg;
634   PropertyObject<double>  _glideslopeOffset_norm;
635   SGVec3d _gsAxis;
636   SGVec3d _gsVertical;
637 };
638
639 GS::ServiceVolume::ServiceVolume()
640 {
641 // maybe this: http://www.tpub.com/content/aviation2/P-1244/P-12440125.htm
642   // ICAO Annex 10 - 3.1.5.2.2: The emission from the glide path equipment
643   // shall be horizontally polarized
644   // very rough abstraction of a 5-element yagi antenna's
645   // E-plane radiation diagram
646   _azimuthTable.addEntry(   0.0, 1.0 );
647   _azimuthTable.addEntry(  10.0, 1.0 );
648   _azimuthTable.addEntry(  30.0, 0.75 );
649   _azimuthTable.addEntry(  40.0, 0.50 );
650   _azimuthTable.addEntry(  50.0, 0.20 );
651   _azimuthTable.addEntry(  60.0, 0.10 );
652   _azimuthTable.addEntry(  70.0, 0.20 );
653   _azimuthTable.addEntry(  80.0, 0.10 );
654   _azimuthTable.addEntry(  90.0, 0.05 );
655   _azimuthTable.addEntry( 105.0, 0.10 );
656   _azimuthTable.addEntry( 130.0, 0.05 );
657   _azimuthTable.addEntry( 150.0, 0.30 );
658   _azimuthTable.addEntry( 160.0, 0.40 );
659   _azimuthTable.addEntry( 170.0, 0.50 );
660   _azimuthTable.addEntry( 180.0, 0.50 );
661
662   _elevationTable.addEntry(   0.0, 0.1 );
663   _elevationTable.addEntry(  1.05, 1.0 );
664   _elevationTable.addEntry(  7.00, 1.0 );
665   _elevationTable.addEntry(  45.0, 0.3 );
666   _elevationTable.addEntry(  90.0, 0.1 );
667   _elevationTable.addEntry( 180.0, 0.01 );
668 }
669
670 double GS::ServiceVolume::adjustRange( double azimuthAngle_deg, double elevationAngle_deg )
671 {
672     return _azimuthTable.interpolate( fabs(azimuthAngle_deg) ) * 
673         _elevationTable.interpolate( fabs(elevationAngle_deg) );
674 }
675
676 GS::GS( SGPropertyNode_ptr rootNode) :
677   NavRadioComponent("gs", rootNode ),
678   _targetGlideslope_deg( rootNode->getNode(_name,true)->getNode("slope",true) ),
679   _glideslopeOffset_norm( rootNode->getNode(_name,true)->getNode("offset-norm",true) ),
680   _gsAxis(SGVec3d::zeros()),
681   _gsVertical(SGVec3d::zeros())
682 {
683 }
684
685 GS::~GS()
686 {
687 }
688
689 FGNavList * GS::getNavaidList()
690 {
691   return globals->get_gslist();
692 }
693
694 double GS::getRange_nm(const SGGeod & aircraftPosition)
695 {
696   double elevationAngle = ::atan2(_heightAboveStation_ft*SG_FEET_TO_METER, _trackDistance_m)*SG_RADIANS_TO_DEGREES;
697   double azimuthAngle = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - fmod(_navRecord->get_multiuse(), 1000.0) );
698   return  _navRecord->get_range() * _serviceVolume.adjustRange( azimuthAngle, elevationAngle );
699 }
700
701 // Calculate a Cartesian unit vector in the
702 // local horizontal plane, i.e. tangent to the 
703 // surface of the earth at the local ground zero.
704 // The tangent vector passes through the given  <midpoint> 
705 // and points forward along the given <heading>.
706 // The <heading> is given in degrees.
707 SGVec3d GS::tangentVector(const SGGeod& midpoint, const double heading)
708 {
709   // move 100m away from the midpoint - arbitrary number
710   const double delta(100.0);
711   SGGeod head, tail;
712   double az2;                   // ignored
713   SGGeodesy::direct(midpoint, heading,     delta, head, az2);
714   SGGeodesy::direct(midpoint, 180+heading, delta, tail, az2);
715   head.setElevationM(midpoint.getElevationM());
716   tail.setElevationM(midpoint.getElevationM());
717   SGVec3d head_xyz = SGVec3d::fromGeod(head);
718   SGVec3d tail_xyz = SGVec3d::fromGeod(tail);
719 // Awkward formula here, needed because vector-by-scalar
720 // multiplication is defined, but not vector-by-scalar division.
721   return (head_xyz - tail_xyz) * (0.5/delta);
722 }
723
724 void GS::search( double frequency, const SGGeod & aircraftPosition )
725 {
726   NavRadioComponent::search( frequency, aircraftPosition );
727   if( false == valid() ) {
728       _gsAxis = SGVec3d::zeros();
729       _gsVertical = SGVec3d::zeros();
730       _targetGlideslope_deg = 3.0;
731       return;
732   }
733   
734   double gs_radial = SGMiscd::normalizePeriodic(0.0, 360.0, fmod(_navRecord->get_multiuse(), 1000.0) );
735
736   _gsAxis = tangentVector(_navRecord->geod(), gs_radial);
737   SGVec3d gsBaseline = tangentVector(_navRecord->geod(), gs_radial + 90.0);
738   _gsVertical = cross(gsBaseline, _gsAxis);
739
740   int tmp = (int)(_navRecord->get_multiuse() / 1000.0);
741   // catch unconfigured glideslopes here, they will cause nan later
742   _targetGlideslope_deg = SGMiscd::max( 1.0, (double)tmp / 100.0 );
743 }
744
745 void GS::update( double dt, const SGGeod & aircraftPosition )
746 {
747   NavRadioComponent::update( dt, aircraftPosition );
748   if( false == valid() ) {
749       _glideslopeOffset_norm = 0.0;
750       return;
751   }
752   
753   SGVec3d pos = SGVec3d::fromGeod(aircraftPosition) - _navRecord->cart(); // relative vector from gs antenna to aircraft
754   // The positive GS axis points along the runway in the landing direction,
755   // toward the far end, not toward the approach area, so we need a - sign here:
756   double comp_h = -dot(pos, _gsAxis);      // component in horiz direction
757   double comp_v = dot(pos, _gsVertical);   // component in vertical direction
758   //double comp_b = dot(pos, _gsBaseline);   // component in baseline direction
759   //if (comp_b) {}                           // ... (useful for debugging)
760
761 // _gsDirect represents the angle of elevation of the aircraft
762 // as seen by the GS transmitter.
763   double gsDirect = atan2(comp_v, comp_h) * SGD_RADIANS_TO_DEGREES;
764 // At this point, if the aircraft is centered on the glide slope,
765 // _gsDirect will be a small positive number, e.g. 3.0 degrees
766
767 // Aim the branch cut straight down 
768 // into the ground below the GS transmitter:
769   if (gsDirect < -90.0) gsDirect += 360.0;
770
771   double offset = _targetGlideslope_deg - gsDirect;
772   if( offset < 0.0 )
773     offset = _targetGlideslope_deg/2 * sawtooth(2.0*offset/_targetGlideslope_deg);
774   assert( false == isnan(offset) );
775 // GS is documented to be 1.4 degrees thick, 
776 // i.e. plus or minus 0.7 degrees from the midline:
777   _glideslopeOffset_norm = SGMiscd::clip(offset/0.7, -1.0, 1.0);
778 }
779
780 void GS::display( NavIndicator & navIndicator )
781 {
782   if( false == valid() ) {
783     navIndicator.setGS( false );
784     return;
785   }
786   navIndicator.setGS( true );
787   navIndicator.setGS( _glideslopeOffset_norm );
788 }
789
790 /* ------------- A NAV/COMM Frequency formatter ---------------------- */
791
792 class FrequencyFormatter : public SGPropertyChangeListener {
793 public:
794   FrequencyFormatter( SGPropertyNode_ptr freqNode, SGPropertyNode_ptr fmtFreqNode, double channelSpacing ) :
795     _freqNode( freqNode ),
796     _fmtFreqNode( fmtFreqNode ),
797     _channelSpacing(channelSpacing)
798   {
799     _freqNode->addChangeListener( this );
800     valueChanged(_freqNode);
801   }
802   ~FrequencyFormatter()
803   {
804     _freqNode->removeChangeListener( this );
805   }
806
807   void valueChanged (SGPropertyNode * prop)
808   {
809     // format as fixed decimal "nnn.nn"
810     std::ostringstream buf;
811     buf << std::fixed 
812         << std::setw(5) 
813         << std::setfill('0') 
814         << std::setprecision(2)
815         << getFrequency();
816     _fmtFreqNode->setStringValue( buf.str() );
817   }
818
819   double getFrequency() const 
820   {
821     double d = SGMiscd::roundToInt(_freqNode->getDoubleValue() / _channelSpacing) * _channelSpacing;
822     // strip last digit, do not round
823     return ((int)(d*100))/100.0;
824   }
825
826 private:
827   SGPropertyNode_ptr _freqNode;
828   SGPropertyNode_ptr _fmtFreqNode;
829   double _channelSpacing;
830 };
831
832
833 /* ------------- The NavRadio implementation ---------------------- */
834
835 class NavRadioImpl : public NavRadio {
836 public:
837   NavRadioImpl( SGPropertyNode_ptr node );
838   virtual ~NavRadioImpl();
839
840   virtual void update( double dt );
841   virtual void init();
842 private:
843   void search();
844
845   class Legacy {
846   public:
847       Legacy( NavRadioImpl * navRadioImpl ) : _navRadioImpl( navRadioImpl ) {}
848
849       void init();
850       void update( double dt );
851   private:
852       NavRadioImpl * _navRadioImpl;
853       SGPropertyNode_ptr is_valid_node;
854       SGPropertyNode_ptr nav_serviceable_node;
855       SGPropertyNode_ptr nav_id_node;
856       SGPropertyNode_ptr id_c1_node;
857       SGPropertyNode_ptr id_c2_node;
858       SGPropertyNode_ptr id_c3_node;
859       SGPropertyNode_ptr id_c4_node;
860   } _legacy;
861
862   const static int VOR_COMPONENT = 0;
863   const static int LOC_COMPONENT = 1;
864   const static int GS_COMPONENT  = 2;
865
866   std::string _name;
867   int         _num;
868   SGPropertyNode_ptr _rootNode;
869   FrequencyFormatter _useFrequencyFormatter;
870   FrequencyFormatter _stbyFrequencyFormatter;
871   std::vector<NavRadioComponent*> _components;
872   NavIndicator _navIndicator;
873   double _stationTTL;
874   double _frequency;
875   PropertyObject<bool> _cdiDisconnected;
876 };
877
878 NavRadioImpl::NavRadioImpl( SGPropertyNode_ptr node ) :
879   _legacy( this ),
880   _name(node->getStringValue("name", "nav")),
881   _num(node->getIntValue("number", 0)),
882   _rootNode(fgGetNode( string("/instrumentation/") + _name, _num, true)),
883   _useFrequencyFormatter( _rootNode->getNode("frequencies/selected-mhz",true), _rootNode->getNode("frequencies/selected-mhz-fmt",true), 0.05 ),
884   _stbyFrequencyFormatter( _rootNode->getNode("frequencies/standby-mhz",true), _rootNode->getNode("frequencies/standby-mhz-fmt",true), 0.05 ),
885   _navIndicator(_rootNode),
886   _stationTTL(0.0),
887   _frequency(-1.0),
888   _cdiDisconnected(_rootNode->getNode("cdi-disconnected",true))
889 {
890 }
891
892 NavRadioImpl::~NavRadioImpl()
893 {
894   BOOST_FOREACH( NavRadioComponent * p, _components ) {
895     delete p;
896   }
897 }
898
899 void NavRadioImpl::init()
900 {
901   if( 0 < _components.size() )
902     return;
903
904   _components.push_back( new VOR(_rootNode) );
905   _components.push_back( new LOC(_rootNode) );
906   _components.push_back( new GS(_rootNode) );
907
908   _legacy.init();
909 }
910
911 void NavRadioImpl::search()
912 {
913 }
914
915 void NavRadioImpl::update( double dt )
916 {
917   if( dt < SGLimitsd::min() ) return;
918
919   SGGeod position;
920
921   try {
922     position = globals->get_aircraft_position();
923   }
924   catch( std::exception & ) {
925     return;
926   }
927
928   _stationTTL -= dt;
929   if( _frequency != _useFrequencyFormatter.getFrequency() ) {
930       _frequency = _useFrequencyFormatter.getFrequency();
931       _stationTTL = 0.0;
932   }
933
934   BOOST_FOREACH( NavRadioComponent * p, _components ) {
935       if( _stationTTL <= 0.0 )
936           p->search( _frequency, position );
937       p->update( dt, position );
938
939       if( false == _cdiDisconnected )
940           p->display( _navIndicator );
941   }
942
943   if( _stationTTL <= 0.0 )
944       _stationTTL = 30.0;
945
946   _legacy.update( dt );
947 }
948
949 void NavRadioImpl::Legacy::init()
950 {
951     is_valid_node = _navRadioImpl->_rootNode->getChild("data-is-valid", 0, true);
952     nav_serviceable_node = _navRadioImpl->_rootNode->getChild("serviceable", 0, true);
953
954     nav_id_node = _navRadioImpl->_rootNode->getChild("nav-id", 0, true );
955     id_c1_node = _navRadioImpl->_rootNode->getChild("nav-id_asc1", 0, true );
956     id_c2_node = _navRadioImpl->_rootNode->getChild("nav-id_asc2", 0, true );
957     id_c3_node = _navRadioImpl->_rootNode->getChild("nav-id_asc3", 0, true );
958     id_c4_node = _navRadioImpl->_rootNode->getChild("nav-id_asc4", 0, true );
959
960 }
961
962 void NavRadioImpl::Legacy::update( double dt )
963 {
964     is_valid_node->setBoolValue( 
965         _navRadioImpl->_components[VOR_COMPONENT]->valid() || _navRadioImpl->_components[LOC_COMPONENT]->valid()  
966         );
967
968     string ident = _navRadioImpl->_components[VOR_COMPONENT]->getIdent();
969     if( ident.empty() )
970         ident = _navRadioImpl->_components[LOC_COMPONENT]->getIdent();
971
972     nav_id_node->setStringValue( ident );
973
974     ident = simgear::strutils::rpad( ident, 4, ' ' );
975     id_c1_node->setIntValue( (int)ident[0] );
976     id_c2_node->setIntValue( (int)ident[1] );
977     id_c3_node->setIntValue( (int)ident[2] );
978     id_c4_node->setIntValue( (int)ident[3] );
979 }
980
981
982 SGSubsystem * NavRadio::createInstance( SGPropertyNode_ptr rootNode )
983 {
984     // use old navradio code by default
985     if( fgGetBool( "/instrumentation/use-new-navradio", false ) )
986         return new NavRadioImpl( rootNode );
987
988     return new FGNavRadio( rootNode );
989 }
990
991 } // namespace Instrumentation
992