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