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 
31 #include <simgear/math/interpolater.hxx>
32 #include <simgear/sg_inlines.h>
33 #include <simgear/props/propertyObject.hxx>
34 #include <simgear/misc/strutils.hxx>
35 #include <simgear/sound/sample_group.hxx>
36 
37 #include <Main/fg_props.hxx>
38 #include <Navaids/navlist.hxx>
39 #include <Sound/audioident.hxx>
40 
41 #include "navradio.hxx"
42 #include "frequencyformatter.hxx"
43 
44 namespace Instrumentation {
45 
46 using simgear::PropertyObject;
47 
48 /* --------------The Navigation Indicator ----------------------------- */
49 
50 class NavIndicator {
51 public:
NavIndicator(SGPropertyNode * rootNode)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 
~NavIndicator()66   virtual ~NavIndicator() {}
67 
68   /**
69    * set the normalized CDI deflection
70    * @param norm the cdi deflection normalized [-1..1]
71    */
setCDI(double norm)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    */
setGS(double norm)82   void setGS( double norm )
83   {
84       _gsDeflectionNorm = norm;
85       _gsDeflectionDeg = norm * 0.7;
86       _gsDeflection = norm * 3.5;
87   }
88 
setGS(bool enabled)89   void setGS( bool enabled )
90   {
91       _hasGS = enabled;
92       if( !enabled ) {
93         setGS( 0.0 );
94       }
95   }
96 
showFrom(bool on)97   void showFrom( bool on )
98   {
99       _fromFlag = on;
100   }
101 
showTo(bool on)102   void showTo( bool on )
103   {
104       _toFlag = on;
105   }
106 
setSelectedCourse(double course)107   void setSelectedCourse( double course )
108   {
109       _course = course;
110   }
111 
getSelectedCourse() const112   double getSelectedCourse() const
113   {
114       return SGMiscd::normalizePeriodic(0.0, 360.0, _course );
115   }
116 
setSignalQuality(double signalQuality)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;
valid() const146   virtual bool   valid() const { return NULL != _navRecord && true == _serviceable; }
getIdent() const147   virtual const std::string getIdent() const { return _ident; }
148 
149 protected:
150   virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
151   virtual FGNavList::TypeFilter* getNavaidFilter() = 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.
sawtooth(double xx)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<std::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 
getIdentString(const std::string & name,int index)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 
NavRadioComponentWithIdent(const std::string & name,SGPropertyNode_ptr rootNode,AudioIdent * audioIdent)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 }
~NavRadioComponentWithIdent()210 NavRadioComponentWithIdent::~NavRadioComponentWithIdent()
211 {
212   delete _audioIdent;
213 }
214 
update(double dt,const SGGeod & aircraftPosition)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 
NavRadioComponent(const std::string & name,SGPropertyNode_ptr rootNode)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 
~NavRadioComponent()247 NavRadioComponent::~NavRadioComponent()
248 {
249 }
250 
getRange_nm(const SGGeod & aircraftPosition)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 
search(double frequency,const SGGeod & aircraftPosition)259 void NavRadioComponent::search( double frequency, const SGGeod & aircraftPosition )
260 {
261   _navRecord = FGNavList::findByFreq(frequency, aircraftPosition, getNavaidFilter() );
262   if( NULL == _navRecord ) {
263     SG_LOG(SG_INSTR,SG_DEBUG, "No " << _name << " available at " << frequency );
264     _ident = "";
265     return;
266   }
267   SG_LOG(SG_INSTR, SG_INFO,
268          "Using " << _name << " '" << _navRecord->get_ident() << "' for " <<
269          frequency);
270   _ident = _navRecord->ident();
271 }
272 
computeSignalQuality_norm(const SGGeod & aircraftPosition)273 double NavRadioComponent::computeSignalQuality_norm( const SGGeod & aircraftPosition )
274 {
275   if( false == valid() ) return 0.0;
276 
277   double distance_nm = _slantDistance_m * SG_METER_TO_NM;
278   double range_nm = _range_nm;
279 
280   // assume signal quality is 100% up to the published range and
281   // decay with the distance squared further out
282   if ( distance_nm <= range_nm ) return 1.0;
283   return range_nm*range_nm/(distance_nm*distance_nm);
284 }
285 
update(double dt,const SGGeod & aircraftPosition)286 void NavRadioComponent::update( double dt, const SGGeod & aircraftPosition )
287 {
288     if( false == valid() ) {
289       _signalQuality_norm = 0.0;
290       _trueBearingTo_deg = 0.0;
291       _trueBearingFrom_deg = 0.0;
292       _trackDistance_m = 0.0;
293       _slantDistance_m = 0.0;
294       return;
295     }
296 
297     _slantDistance_m = dist(_navRecord->cart(), SGVec3d::fromGeod(aircraftPosition));
298 
299     double az1 = 0.0, az2 = 0.0, dist = 0.0;
300     SGGeodesy::inverse(aircraftPosition, _navRecord->geod(), az1, az2, dist );
301     _trueBearingTo_deg = az1; _trueBearingFrom_deg = az2; _trackDistance_m = dist;
302     _heightAboveStation_ft = SGMiscd::max(0.0, aircraftPosition.getElevationFt() - _navRecord->get_elev_ft());
303 
304     _range_nm = getRange_nm(aircraftPosition);
305     _signalQuality_norm = computeSignalQuality_norm( aircraftPosition );
306     _inRange = _signalQuality_norm > 0.2;
307 }
308 
309 /* ---------------------------------------------------------------- */
310 
VORTablePath(const char * name)311 static SGPath VORTablePath( const char * name )
312 {
313     SGPath path( globals->get_fg_root() );
314     path.append( "Navaids" );
315     path.append(name);
316     return path;
317 }
318 
319 class VOR : public NavRadioComponentWithIdent {
320 public:
321   VOR( SGPropertyNode_ptr rootNode);
322   virtual ~VOR();
323   virtual void update( double dt, const SGGeod & aircraftPosition );
324   virtual void display( NavIndicator & navIndicator );
325   virtual double getRange_nm(const SGGeod & aircraftPosition);
326 protected:
327   virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
328   virtual FGNavList::TypeFilter* getNavaidFilter();
329 
330 private:
331   double _totalTime;
332   class ServiceVolume {
333   public:
ServiceVolume()334     ServiceVolume() :
335       term_tbl(VORTablePath("range.term")),
336       low_tbl(VORTablePath("range.low")),
337       high_tbl(VORTablePath("range.high")) {
338     }
339     double adjustRange( double height_ft, double nominalRange_nm );
340 
341   private:
342     SGInterpTable term_tbl;
343     SGInterpTable low_tbl;
344     SGInterpTable high_tbl;
345   } _serviceVolume;
346 
347   PropertyObject<double> _radial;
348   PropertyObject<double> _radialInbound;
349 };
350 
351 // model standard VOR/DME/TACAN service volumes as per AIM 1-1-8
adjustRange(double height_ft,double nominalRange_nm)352 double VOR::ServiceVolume::adjustRange( double height_ft, double nominalRange_nm )
353 {
354     if (nominalRange_nm < SGLimitsd::min() )
355       nominalRange_nm = FG_NAV_DEFAULT_RANGE;
356 
357     // extend out actual usable range to be 1.3x the published safe range
358     const double usability_factor = 1.3;
359 
360     // assumptions we model the standard service volume, plus
361     // ... rather than specifying a cylinder, we model a cone that
362     // contains the cylinder.  Then we put an upside down cone on top
363     // to model diminishing returns at too-high altitudes.
364 
365     if ( nominalRange_nm < 25.0 + SG_EPSILON ) {
366 	// Standard Terminal Service Volume
367 	return term_tbl.interpolate( height_ft ) * usability_factor;
368     } else if ( nominalRange_nm < 50.0 + SG_EPSILON ) {
369 	// Standard Low Altitude Service Volume
370 	// table is based on range of 40, scale to actual range
371 	return low_tbl.interpolate( height_ft ) * nominalRange_nm / 40.0
372 	    * usability_factor;
373     } else {
374 	// Standard High Altitude Service Volume
375 	// table is based on range of 130, scale to actual range
376 	return high_tbl.interpolate( height_ft ) * nominalRange_nm / 130.0
377 	    * usability_factor;
378     }
379 }
380 
VOR(SGPropertyNode_ptr rootNode)381 VOR::VOR( SGPropertyNode_ptr rootNode) :
382     NavRadioComponentWithIdent("vor", rootNode,
383                                new VORAudioIdent(getIdentString(std::string("vor"),
384                                                                 rootNode->getIndex()))),
385   _totalTime(0.0),
386   _radial( rootNode->getNode(_name,true)->getNode("radial",true) ),
387   _radialInbound( rootNode->getNode(_name,true)->getNode("radial-inbound",true) )
388 {
389 }
390 
~VOR()391 VOR::~VOR()
392 {
393 }
394 
getRange_nm(const SGGeod & aircraftPosition)395 double VOR::getRange_nm( const SGGeod & aircraftPosition )
396 {
397   return _serviceVolume.adjustRange( _heightAboveStation_ft, _navRecord->get_range() );
398 }
399 
getNavaidFilter()400 FGNavList::TypeFilter* VOR::getNavaidFilter()
401 {
402   static FGNavList::TypeFilter filter(FGPositioned::VOR);
403   return &filter;
404 }
405 
computeSignalQuality_norm(const SGGeod & aircraftPosition)406 double VOR::computeSignalQuality_norm( const SGGeod & aircraftPosition )
407 {
408   // apply cone of confusion. Some sources say it's opening angle is 53deg, others estimate
409   // a diameter of 1NM per 6000ft (approx. 45deg). ICAO Annex 10 says minimum 40deg.
410   // We use 1NM@6000ft and a distance-squared
411   // function to make signal-quality=100% 0.5NM@6000ft from the center and zero overhead
412   double cone_of_confusion_width = 0.5 * _heightAboveStation_ft / 6000.0 * SG_NM_TO_METER;
413   if( _trackDistance_m < cone_of_confusion_width ) {
414     double d = cone_of_confusion_width <= SGLimitsd::min() ? 1 :
415               (1 - _trackDistance_m/cone_of_confusion_width);
416     return 1-d*d;
417   }
418 
419   // use default decay function outside the cone of confusion
420   return NavRadioComponentWithIdent::computeSignalQuality_norm( aircraftPosition );
421 }
422 
update(double dt,const SGGeod & aircraftPosition)423 void VOR::update( double dt, const SGGeod & aircraftPosition )
424 {
425   _totalTime += dt;
426   NavRadioComponentWithIdent::update( dt, aircraftPosition );
427 
428   if( false == valid() ) {
429       _radial = 0.0;
430       return;
431   }
432 
433   // an arbitrary error function
434   double error = 0.5*(sin(_totalTime/11.0) + sin(_totalTime/23.0));
435 
436   // add 1% error at 100% signal-quality
437   // add 50% error at  0% signal-quality
438   // of full deflection (+/-10deg)
439   double e = 10.0 * ( 0.01 + (1-_signalQuality_norm) * 0.49 ) * error;
440 
441   // compute magnetic bearing from the station (aka current radial)
442   double r = SGMiscd::normalizePeriodic(0.0, 360.0, _trueBearingFrom_deg - _navRecord->get_multiuse() + e );
443 
444   _radial = r;
445   _radialInbound = SGMiscd::normalizePeriodic(0.0,360.0, 180.0 + _radial);
446 }
447 
display(NavIndicator & navIndicator)448 void VOR::display( NavIndicator & navIndicator )
449 {
450   if( false == valid() ) return;
451 
452   double offset = SGMiscd::normalizePeriodic(-180.0,180.0,_radial - navIndicator.getSelectedCourse());
453   bool to = fabs(offset) >= 90.0;
454 
455   if( to ) offset = -offset + copysign(180.0,offset);
456 
457   navIndicator.showTo( to );
458   navIndicator.showFrom( !to );
459   // normalize to +/- 1.0 for +/- 10deg, decrease deflection with decreasing signal
460   navIndicator.setCDI( SGMiscd::clip( -offset/10.0, -1.0, 1.0 ) * _signalQuality_norm );
461   navIndicator.setSignalQuality( _signalQuality_norm );
462 }
463 
464 /* ---------------------------------------------------------------- */
465 class LOC : public NavRadioComponentWithIdent {
466 public:
467   LOC( SGPropertyNode_ptr rootNode );
468   virtual ~LOC();
469   virtual void update( double dt, const SGGeod & aircraftPosition );
470   virtual void search( double frequency, const SGGeod & aircraftPosition );
471   virtual void display( NavIndicator & navIndicator );
472   virtual double getRange_nm(const SGGeod & aircraftPosition);
473 
474 protected:
475   virtual double computeSignalQuality_norm( const SGGeod & aircraftPosition );
476   virtual FGNavList::TypeFilter* getNavaidFilter();
477 
478 private:
479   class ServiceVolume {
480   public:
481       ServiceVolume();
482       double adjustRange( double azimuthAngle_deg, double elevationAngle_deg );
483   private:
484       SGInterpTable _azimuthTable;
485       SGInterpTable _elevationTable;
486   } _serviceVolume;
487   PropertyObject<double> _localizerOffset_norm;
488   PropertyObject<double> _localizerOffset_m;
489   PropertyObject<double> _localizerWidth_deg;
490 };
491 
ServiceVolume()492 LOC::ServiceVolume::ServiceVolume()
493 {
494 // maybe this: http://www.tpub.com/content/aviation2/P-1244/P-12440125.htm
495   // ICAO Annex 10 - 3.1.3.2.2: The emission from the localizer
496   // shall be horizontally polarized
497   // very rough abstraction of a 5-element yagi antenna's
498   // E-plane radiation diagram
499   _azimuthTable.addEntry(   0.0, 1.0 );
500   _azimuthTable.addEntry(  10.0, 1.0 );
501   _azimuthTable.addEntry(  30.0, 0.75 );
502   _azimuthTable.addEntry(  40.0, 0.50 );
503   _azimuthTable.addEntry(  50.0, 0.20 );
504   _azimuthTable.addEntry(  60.0, 0.10 );
505   _azimuthTable.addEntry(  70.0, 0.20 );
506   _azimuthTable.addEntry(  80.0, 0.10 );
507   _azimuthTable.addEntry(  90.0, 0.05 );
508   _azimuthTable.addEntry( 105.0, 0.10 );
509   _azimuthTable.addEntry( 130.0, 0.05 );
510   _azimuthTable.addEntry( 150.0, 0.30 );
511   _azimuthTable.addEntry( 160.0, 0.40 );
512   _azimuthTable.addEntry( 170.0, 0.50 );
513   _azimuthTable.addEntry( 180.0, 0.50 );
514 
515   _elevationTable.addEntry(   0.0, 0.1 );
516   _elevationTable.addEntry(  1.05, 1.0 );
517   _elevationTable.addEntry(  7.00, 1.0 );
518   _elevationTable.addEntry(  45.0, 0.3 );
519   _elevationTable.addEntry(  90.0, 0.1 );
520   _elevationTable.addEntry( 180.0, 0.01 );
521 }
522 
adjustRange(double azimuthAngle_deg,double elevationAngle_deg)523 double LOC::ServiceVolume::adjustRange( double azimuthAngle_deg, double elevationAngle_deg )
524 {
525     return _azimuthTable.interpolate( fabs(azimuthAngle_deg) ) *
526         _elevationTable.interpolate( fabs(elevationAngle_deg) );
527 }
528 
LOC(SGPropertyNode_ptr rootNode)529 LOC::LOC( SGPropertyNode_ptr rootNode) :
530     NavRadioComponentWithIdent("loc", rootNode, new LOCAudioIdent(getIdentString(std::string("loc"),
531                                                                                  rootNode->getIndex()))),
532   _serviceVolume(),
533   _localizerOffset_norm( rootNode->getNode(_name,true)->getNode("offset-norm",true) ),
534   _localizerOffset_m( rootNode->getNode(_name,true)->getNode("offset-m",true) ),
535   _localizerWidth_deg( rootNode->getNode(_name,true)->getNode("width-deg",true) )
536 {
537 }
538 
~LOC()539 LOC::~LOC()
540 {
541 }
542 
getNavaidFilter()543 FGNavList::TypeFilter* LOC::getNavaidFilter()
544 {
545   return FGNavList::locFilter();
546 }
547 
search(double frequency,const SGGeod & aircraftPosition)548 void LOC::search( double frequency, const SGGeod & aircraftPosition )
549 {
550   NavRadioComponentWithIdent::search( frequency, aircraftPosition );
551   if( false == valid() ) {
552       _localizerWidth_deg = 0.0;
553       return;
554   }
555 
556   // cache slightly expensive value,
557   // sanitized in FGNavRecord::localizerWidth() to  never become zero
558   _localizerWidth_deg = _navRecord->localizerWidth();
559 }
560 
561 /* Localizer coverage (ICAO Annex 10 Volume I 3.1.3.3
562   25NM within +/-10 deg from the front course line
563   17NM between 10 and 35deg from the front course line
564   10NM outside of +/- 35deg  if coverage is provided
565   at and above a height of 2000ft above threshold or
566   1000ft above the highest point within intermediate
567   and final approach areas. Upper limit is a surface
568   extending outward from the localizer and inclined at
569   7 degrees above the horizontal
570  */
getRange_nm(const SGGeod & aircraftPosition)571 double LOC::getRange_nm(const SGGeod & aircraftPosition)
572 {
573   double elevationAngle = ::atan2(_heightAboveStation_ft*SG_FEET_TO_METER, _trackDistance_m)*SG_RADIANS_TO_DEGREES;
574   double azimuthAngle = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - _navRecord->get_multiuse() );
575 
576   // looks like our navrecord declared range is based on 10NM?
577   return  _navRecord->get_range() * _serviceVolume.adjustRange( azimuthAngle, elevationAngle );
578 }
579 
computeSignalQuality_norm(const SGGeod & aircraftPosition)580 double LOC::computeSignalQuality_norm( const SGGeod & aircraftPosition )
581 {
582   return NavRadioComponentWithIdent::computeSignalQuality_norm( aircraftPosition );
583 }
584 
update(double dt,const SGGeod & aircraftPosition)585 void LOC::update( double dt, const SGGeod & aircraftPosition )
586 {
587   NavRadioComponentWithIdent::update( dt, aircraftPosition );
588 
589   if( false == valid() ) {
590     _localizerOffset_norm = 0.0;
591     _localizerOffset_m = 0.0;
592     return;
593   }
594 
595   double offsetDeg = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - _navRecord->get_multiuse() );
596 
597   // cross-track error (in meters)
598   _localizerOffset_m = _trackDistance_m * sin(offsetDeg * SGD_DEGREES_TO_RADIANS);
599 
600   // The factor of 30.0 gives a period of 120 which gives us 3 cycles and six
601   // zeros i.e. six courses: one front course, one back course, and four
602   // false courses. Three of the six are reverse sensing.
603   offsetDeg = 30.0 * sawtooth(offsetDeg / 30.0);
604 
605   // normalize offsetDeg to the localizer width, scale and clip to [-1..1]
606   offsetDeg = SGMiscd::clip( 2.0 * offsetDeg / _localizerWidth_deg, -1.0, 1.0 );
607 
608   _localizerOffset_norm = offsetDeg;
609 }
610 
display(NavIndicator & navIndicator)611 void LOC::display( NavIndicator & navIndicator )
612 {
613   if( false == valid() )
614     return;
615 
616   navIndicator.showTo( true );
617   navIndicator.showFrom( false );
618 
619   navIndicator.setCDI( _localizerOffset_norm * _signalQuality_norm );
620   navIndicator.setSignalQuality( _signalQuality_norm );
621 }
622 
623 class GS : public NavRadioComponent {
624 public:
625   GS( SGPropertyNode_ptr rootNode);
626   virtual ~GS();
627   virtual void update( double dt, const SGGeod & aircraftPosition );
628   virtual void search( double frequency, const SGGeod & aircraftPosition );
629   virtual void display( NavIndicator & navIndicator );
630 
631   virtual double getRange_nm(const SGGeod & aircraftPosition);
632 protected:
633   virtual FGNavList::TypeFilter* getNavaidFilter();
634 private:
635   class ServiceVolume {
636   public:
637       ServiceVolume();
638       double adjustRange( double azimuthAngle_deg, double elevationAngle_deg );
639   private:
640       SGInterpTable _azimuthTable;
641       SGInterpTable _elevationTable;
642   } _serviceVolume;
643   static SGVec3d tangentVector(const SGGeod& midpoint, const double heading);
644 
645   PropertyObject<double>  _targetGlideslope_deg;
646   PropertyObject<double>  _glideslopeOffset_norm;
647   SGVec3d _gsAxis;
648   SGVec3d _gsVertical;
649 };
650 
ServiceVolume()651 GS::ServiceVolume::ServiceVolume()
652 {
653 // maybe this: http://www.tpub.com/content/aviation2/P-1244/P-12440125.htm
654   // ICAO Annex 10 - 3.1.5.2.2: The emission from the glide path equipment
655   // shall be horizontally polarized
656   // very rough abstraction of a 5-element yagi antenna's
657   // E-plane radiation diagram
658   _azimuthTable.addEntry(   0.0, 1.0 );
659   _azimuthTable.addEntry(  10.0, 1.0 );
660   _azimuthTable.addEntry(  30.0, 0.75 );
661   _azimuthTable.addEntry(  40.0, 0.50 );
662   _azimuthTable.addEntry(  50.0, 0.20 );
663   _azimuthTable.addEntry(  60.0, 0.10 );
664   _azimuthTable.addEntry(  70.0, 0.20 );
665   _azimuthTable.addEntry(  80.0, 0.10 );
666   _azimuthTable.addEntry(  90.0, 0.05 );
667   _azimuthTable.addEntry( 105.0, 0.10 );
668   _azimuthTable.addEntry( 130.0, 0.05 );
669   _azimuthTable.addEntry( 150.0, 0.30 );
670   _azimuthTable.addEntry( 160.0, 0.40 );
671   _azimuthTable.addEntry( 170.0, 0.50 );
672   _azimuthTable.addEntry( 180.0, 0.50 );
673 
674   _elevationTable.addEntry(   0.0, 0.1 );
675   _elevationTable.addEntry(  1.05, 1.0 );
676   _elevationTable.addEntry(  7.00, 1.0 );
677   _elevationTable.addEntry(  45.0, 0.3 );
678   _elevationTable.addEntry(  90.0, 0.1 );
679   _elevationTable.addEntry( 180.0, 0.01 );
680 }
681 
adjustRange(double azimuthAngle_deg,double elevationAngle_deg)682 double GS::ServiceVolume::adjustRange( double azimuthAngle_deg, double elevationAngle_deg )
683 {
684     return _azimuthTable.interpolate( fabs(azimuthAngle_deg) ) *
685         _elevationTable.interpolate( fabs(elevationAngle_deg) );
686 }
687 
GS(SGPropertyNode_ptr rootNode)688 GS::GS( SGPropertyNode_ptr rootNode) :
689   NavRadioComponent("gs", rootNode ),
690   _targetGlideslope_deg( rootNode->getNode(_name,true)->getNode("slope",true) ),
691   _glideslopeOffset_norm( rootNode->getNode(_name,true)->getNode("offset-norm",true) ),
692   _gsAxis(SGVec3d::zeros()),
693   _gsVertical(SGVec3d::zeros())
694 {
695 }
696 
~GS()697 GS::~GS()
698 {
699 }
700 
getNavaidFilter()701 FGNavList::TypeFilter* GS::getNavaidFilter()
702 {
703   static FGNavList::TypeFilter filter(FGPositioned::GS);
704   return &filter;
705 }
706 
getRange_nm(const SGGeod & aircraftPosition)707 double GS::getRange_nm(const SGGeod & aircraftPosition)
708 {
709   double elevationAngle = ::atan2(_heightAboveStation_ft*SG_FEET_TO_METER, _trackDistance_m)*SG_RADIANS_TO_DEGREES;
710   double azimuthAngle = SGMiscd::normalizePeriodic( -180.0, 180.0, _trueBearingFrom_deg + 180.0 - fmod(_navRecord->get_multiuse(), 1000.0) );
711   return  _navRecord->get_range() * _serviceVolume.adjustRange( azimuthAngle, elevationAngle );
712 }
713 
714 // Calculate a Cartesian unit vector in the
715 // local horizontal plane, i.e. tangent to the
716 // surface of the earth at the local ground zero.
717 // The tangent vector passes through the given  <midpoint>
718 // and points forward along the given <heading>.
719 // The <heading> is given in degrees.
tangentVector(const SGGeod & midpoint,const double heading)720 SGVec3d GS::tangentVector(const SGGeod& midpoint, const double heading)
721 {
722   // move 100m away from the midpoint - arbitrary number
723   const double delta(100.0);
724   SGGeod head, tail;
725   double az2;                   // ignored
726   SGGeodesy::direct(midpoint, heading,     delta, head, az2);
727   SGGeodesy::direct(midpoint, 180+heading, delta, tail, az2);
728   head.setElevationM(midpoint.getElevationM());
729   tail.setElevationM(midpoint.getElevationM());
730   SGVec3d head_xyz = SGVec3d::fromGeod(head);
731   SGVec3d tail_xyz = SGVec3d::fromGeod(tail);
732 // Awkward formula here, needed because vector-by-scalar
733 // multiplication is defined, but not vector-by-scalar division.
734   return (head_xyz - tail_xyz) * (0.5/delta);
735 }
736 
search(double frequency,const SGGeod & aircraftPosition)737 void GS::search( double frequency, const SGGeod & aircraftPosition )
738 {
739   NavRadioComponent::search( frequency, aircraftPosition );
740   if( false == valid() ) {
741       _gsAxis = SGVec3d::zeros();
742       _gsVertical = SGVec3d::zeros();
743       _targetGlideslope_deg = 3.0;
744       return;
745   }
746 
747   double gs_radial = SGMiscd::normalizePeriodic(0.0, 360.0, fmod(_navRecord->get_multiuse(), 1000.0) );
748 
749   _gsAxis = tangentVector(_navRecord->geod(), gs_radial);
750   SGVec3d gsBaseline = tangentVector(_navRecord->geod(), gs_radial + 90.0);
751   _gsVertical = cross(gsBaseline, _gsAxis);
752 
753   int tmp = (int)(_navRecord->get_multiuse() / 1000.0);
754   // catch unconfigured glideslopes here, they will cause nan later
755   _targetGlideslope_deg = SGMiscd::max( 1.0, (double)tmp / 100.0 );
756 }
757 
update(double dt,const SGGeod & aircraftPosition)758 void GS::update( double dt, const SGGeod & aircraftPosition )
759 {
760   NavRadioComponent::update( dt, aircraftPosition );
761   if( false == valid() ) {
762       _glideslopeOffset_norm = 0.0;
763       return;
764   }
765 
766   SGVec3d pos = SGVec3d::fromGeod(aircraftPosition) - _navRecord->cart(); // relative vector from gs antenna to aircraft
767   // The positive GS axis points along the runway in the landing direction,
768   // toward the far end, not toward the approach area, so we need a - sign here:
769   double comp_h = -dot(pos, _gsAxis);      // component in horiz direction
770   double comp_v = dot(pos, _gsVertical);   // component in vertical direction
771   //double comp_b = dot(pos, _gsBaseline);   // component in baseline direction
772   //if (comp_b) {}                           // ... (useful for debugging)
773 
774 // _gsDirect represents the angle of elevation of the aircraft
775 // as seen by the GS transmitter.
776   double gsDirect = atan2(comp_v, comp_h) * SGD_RADIANS_TO_DEGREES;
777 // At this point, if the aircraft is centered on the glide slope,
778 // _gsDirect will be a small positive number, e.g. 3.0 degrees
779 
780 // Aim the branch cut straight down
781 // into the ground below the GS transmitter:
782   if (gsDirect < -90.0) gsDirect += 360.0;
783 
784   double offset = _targetGlideslope_deg - gsDirect;
785   if( offset < 0.0 )
786     offset = _targetGlideslope_deg/2 * sawtooth(2.0*offset/_targetGlideslope_deg);
787     assert( false == SGMisc<double>::isNaN(offset) );
788 // GS is documented to be 1.4 degrees thick,
789 // i.e. plus or minus 0.7 degrees from the midline:
790   _glideslopeOffset_norm = SGMiscd::clip(offset/0.7, -1.0, 1.0);
791 }
792 
display(NavIndicator & navIndicator)793 void GS::display( NavIndicator & navIndicator )
794 {
795   if( false == valid() ) {
796     navIndicator.setGS( false );
797     return;
798   }
799   navIndicator.setGS( true );
800   navIndicator.setGS( _glideslopeOffset_norm );
801 }
802 
803 /* ------------- The NavRadio implementation ---------------------- */
804 
805 class NavRadioImpl : public NavRadio
806 {
807 public:
808     NavRadioImpl( SGPropertyNode_ptr node );
809     virtual ~NavRadioImpl();
810 
811     // Subsystem API.
812     void init() override;
813     void update(double dt) override;
814 
815 private:
816     void search();
817 
818     class Legacy {
819     public:
Legacy(NavRadioImpl * navRadioImpl)820         Legacy( NavRadioImpl * navRadioImpl ) : _navRadioImpl( navRadioImpl ) {}
821 
822         void init();
823         void update( double dt );
824 
825     private:
826         NavRadioImpl * _navRadioImpl;
827         SGPropertyNode_ptr is_valid_node;
828         SGPropertyNode_ptr nav_serviceable_node;
829         SGPropertyNode_ptr nav_id_node;
830         SGPropertyNode_ptr id_c1_node;
831         SGPropertyNode_ptr id_c2_node;
832         SGPropertyNode_ptr id_c3_node;
833         SGPropertyNode_ptr id_c4_node;
834     } _legacy;
835 
836     const static int VOR_COMPONENT = 0;
837     const static int LOC_COMPONENT = 1;
838     const static int GS_COMPONENT  = 2;
839 
840     std::string _name;
841     int         _num;
842     SGPropertyNode_ptr _rootNode;
843     FrequencyFormatter _useFrequencyFormatter;
844     FrequencyFormatter _stbyFrequencyFormatter;
845     std::vector<NavRadioComponent*> _components;
846     NavIndicator _navIndicator;
847     double _stationTTL;
848     double _frequency;
849     PropertyObject<bool> _cdiDisconnected;
850     PropertyObject<std::string> _navType;
851 };
852 
NavRadioImpl(SGPropertyNode_ptr node)853 NavRadioImpl::NavRadioImpl( SGPropertyNode_ptr node ) :
854   _legacy( this ),
855   _name(node->getStringValue("name", "nav")),
856   _num(node->getIntValue("number", 0)),
857   _rootNode(fgGetNode( std::string("/instrumentation/") + _name, _num, true)),
858   _useFrequencyFormatter( _rootNode->getNode("frequencies/selected-mhz",true), _rootNode->getNode("frequencies/selected-mhz-fmt",true), 0.05, 108.0, 118.0 ),
859   _stbyFrequencyFormatter( _rootNode->getNode("frequencies/standby-mhz",true), _rootNode->getNode("frequencies/standby-mhz-fmt",true), 0.05, 108.0, 118.0 ),
860   _navIndicator(_rootNode),
861   _stationTTL(0.0),
862   _frequency(-1.0),
863   _cdiDisconnected(_rootNode->getNode("cdi-disconnected",true)),
864   _navType(_rootNode->getNode("nav-type",true))
865 {
866 }
867 
~NavRadioImpl()868 NavRadioImpl::~NavRadioImpl()
869 {
870   for( auto p : _components ) {
871     delete p;
872   }
873 }
874 
init()875 void NavRadioImpl::init()
876 {
877   if( ! _components.empty() )
878     return;
879 
880   _components.push_back( new VOR(_rootNode) );
881   _components.push_back( new LOC(_rootNode) );
882   _components.push_back( new GS(_rootNode) );
883 
884   _legacy.init();
885 }
886 
search()887 void NavRadioImpl::search()
888 {
889 }
890 
update(double dt)891 void NavRadioImpl::update( double dt )
892 {
893   if( dt < SGLimitsd::min() ) return;
894 
895   SGGeod position;
896 
897   try {
898     position = globals->get_aircraft_position();
899   }
900   catch( std::exception & ) {
901     return;
902   }
903 
904   _stationTTL -= dt;
905   if( _frequency != _useFrequencyFormatter.getFrequency() ) {
906       _frequency = _useFrequencyFormatter.getFrequency();
907       _stationTTL = 0.0;
908   }
909 
910   for( auto p : _components ) {
911       if( _stationTTL <= 0.0 )
912           p->search( _frequency, position );
913       p->update( dt, position );
914 
915       if( false == _cdiDisconnected )
916           p->display( _navIndicator );
917   }
918 
919   if( _stationTTL <= 0.0 )
920       _stationTTL = 30.0;
921 
922   if( _components[VOR_COMPONENT]->valid() ) {
923     _navType = "vor";
924   } else if( _components[LOC_COMPONENT]->valid() ) {
925     _navType = "loc";
926   } else {
927     _navType = "";
928   }
929 
930   _legacy.update( dt );
931 }
932 
init()933 void NavRadioImpl::Legacy::init()
934 {
935     is_valid_node = _navRadioImpl->_rootNode->getChild("data-is-valid", 0, true);
936     nav_serviceable_node = _navRadioImpl->_rootNode->getChild("serviceable", 0, true);
937 
938     nav_id_node = _navRadioImpl->_rootNode->getChild("nav-id", 0, true );
939     id_c1_node = _navRadioImpl->_rootNode->getChild("nav-id_asc1", 0, true );
940     id_c2_node = _navRadioImpl->_rootNode->getChild("nav-id_asc2", 0, true );
941     id_c3_node = _navRadioImpl->_rootNode->getChild("nav-id_asc3", 0, true );
942     id_c4_node = _navRadioImpl->_rootNode->getChild("nav-id_asc4", 0, true );
943 
944 }
945 
update(double dt)946 void NavRadioImpl::Legacy::update( double dt )
947 {
948     is_valid_node->setBoolValue(
949         _navRadioImpl->_components[VOR_COMPONENT]->valid() || _navRadioImpl->_components[LOC_COMPONENT]->valid()
950         );
951 
952     std::string ident = _navRadioImpl->_components[VOR_COMPONENT]->getIdent();
953     if( ident.empty() )
954         ident = _navRadioImpl->_components[LOC_COMPONENT]->getIdent();
955 
956     nav_id_node->setStringValue( ident );
957 
958     ident = simgear::strutils::rpad( ident, 4, ' ' );
959     id_c1_node->setIntValue( (int)ident[0] );
960     id_c2_node->setIntValue( (int)ident[1] );
961     id_c3_node->setIntValue( (int)ident[2] );
962     id_c4_node->setIntValue( (int)ident[3] );
963 }
964 
965 
createInstance(SGPropertyNode_ptr rootNode)966 SGSubsystem * NavRadio::createInstance( SGPropertyNode_ptr rootNode )
967 {
968     // use old navradio code by default
969     if( fgGetBool( "/instrumentation/use-new-navradio", false ) )
970         return new NavRadioImpl( rootNode );
971 
972     return new FGNavRadio( rootNode );
973 }
974 
975 
976 // Register the subsystem.
977 #if 0
978 SGSubsystemMgr::InstancedRegistrant<NavRadio> registrantNavRadio(
979     SGSubsystemMgr::FDM,
980     {{"instrumentation", SGSubsystemMgr::Dependency::HARD}});
981 #endif
982 
983 } // namespace Instrumentation
984 
985